diff options
Diffstat (limited to 'core/src/main/java')
67 files changed, 1363 insertions, 660 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java new file mode 100644 index 000000000..f7757935a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/DiscoveryDefaultUpdateEvent.java @@ -0,0 +1,6 @@ +package de.danoeh.antennapod.core.event; + +public class DiscoveryDefaultUpdateEvent { + public DiscoveryDefaultUpdateEvent() { + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java index 24a71ec96..efd53ab9d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.event; +import androidx.annotation.NonNull; + import java.util.ArrayList; import java.util.List; @@ -19,6 +21,7 @@ public class DownloadEvent { return new DownloadEvent(update); } + @NonNull @Override public String toString() { return "DownloadEvent{" + diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java index 7428c5b00..5ab5decf9 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadLogEvent.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.event; +import androidx.annotation.NonNull; + public class DownloadLogEvent { private DownloadLogEvent() { @@ -9,6 +11,7 @@ public class DownloadLogEvent { return new DownloadLogEvent(); } + @NonNull @Override public String toString() { return "DownloadLogEvent"; diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java index f549940b7..10992408d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java @@ -46,6 +46,7 @@ public class DownloaderUpdate { this.mediaIds = mediaIds1.toArray(); } + @NonNull @Override public String toString() { return "DownloaderUpdate{" + diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java index 578007561..d3be8fac0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.event; +import androidx.annotation.NonNull; + import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -27,6 +29,7 @@ public class FavoritesEvent { return new FavoritesEvent(Action.REMOVED, item); } + @NonNull @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java index 4b14a72d2..02559b2f5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java @@ -21,7 +21,7 @@ public class FeedItemEvent { private final Action action; @NonNull public final List<FeedItem> items; - private FeedItemEvent(Action action, List<FeedItem> items) { + private FeedItemEvent(@NonNull Action action, @NonNull List<FeedItem> items) { this.action = action; this.items = items; } @@ -42,6 +42,7 @@ public class FeedItemEvent { return updated(Arrays.asList(items)); } + @NonNull @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java b/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java new file mode 100644 index 000000000..f33fa7511 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/ChapterMerger.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.core.feed; + +import android.text.TextUtils; +import android.util.Log; +import androidx.annotation.Nullable; + +import java.util.List; + +public class ChapterMerger { + private static final String TAG = "ChapterMerger"; + + private ChapterMerger() { + + } + + /** + * This method might modify the input data. + */ + @Nullable + public static List<Chapter> merge(@Nullable List<Chapter> chapters1, @Nullable List<Chapter> chapters2) { + Log.d(TAG, "Merging chapters"); + if (chapters1 == null) { + return chapters2; + } else if (chapters2 == null) { + return chapters1; + } else if (chapters2.size() > chapters1.size()) { + return chapters2; + } else if (chapters2.size() < chapters1.size()) { + return chapters1; + } else { + // Merge chapter lists of same length. Store in chapters2 array. + // In case the lists can not be merged, return chapters1 array. + for (int i = 0; i < chapters2.size(); i++) { + Chapter chapterTarget = chapters2.get(i); + Chapter chapterOther = chapters1.get(i); + + if (Math.abs(chapterTarget.start - chapterOther.start) > 1000) { + Log.e(TAG, "Chapter lists are too different. Cancelling merge."); + return chapters1; + } + + if (TextUtils.isEmpty(chapterTarget.imageUrl)) { + chapterTarget.imageUrl = chapterOther.imageUrl; + } + if (TextUtils.isEmpty(chapterTarget.link)) { + chapterTarget.link = chapterOther.link; + } + if (TextUtils.isEmpty(chapterTarget.title)) { + chapterTarget.title = chapterOther.title; + } + } + return chapters2; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java index 0889e5182..a3b66c951 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java @@ -24,6 +24,7 @@ public class Feed extends FeedFile implements ImageResource { public static final int FEEDFILETYPE_FEED = 0; public static final String TYPE_RSS2 = "rss"; public static final String TYPE_ATOM1 = "atom"; + public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:"; /* title as defined by the feed */ private String feedTitle; @@ -352,7 +353,7 @@ public class Feed extends FeedFile implements ImageResource { Date mostRecentDate = new Date(0); FeedItem mostRecentItem = null; for (FeedItem item : items) { - if (item.getPubDate().after(mostRecentDate)) { + if (item.getPubDate() != null && item.getPubDate().after(mostRecentDate)) { mostRecentDate = item.getPubDate(); mostRecentItem = item; } @@ -551,4 +552,7 @@ public class Feed extends FeedFile implements ImageResource { this.lastUpdateFailed = lastUpdateFailed; } + public boolean isLocalFeed() { + return download_url.startsWith(PREFIX_LOCAL_FOLDER); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java index 2610d253f..3edecd35c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java @@ -50,7 +50,7 @@ public abstract class FeedComponent { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || !(o instanceof FeedComponent)) return false; + if (!(o instanceof FeedComponent)) return false; FeedComponent that = (FeedComponent) o; diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java index 15cdf92dc..044554451 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedEvent.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.feed; +import androidx.annotation.NonNull; + import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; @@ -18,6 +20,7 @@ public class FeedEvent { this.feedId = feedId; } + @NonNull @Override public String toString() { return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java index 20ed402fc..131cbe563 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -1,12 +1,15 @@ package de.danoeh.antennapod.core.feed; import android.database.Cursor; + +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import android.text.TextUtils; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; +import java.io.Serializable; import java.util.Date; import java.util.HashSet; import java.util.List; @@ -24,7 +27,7 @@ import de.danoeh.antennapod.core.util.ShownotesProvider; * * @author daniel */ -public class FeedItem extends FeedComponent implements ShownotesProvider, ImageResource { +public class FeedItem extends FeedComponent implements ShownotesProvider, ImageResource, Serializable { /** tag that indicates this item is in the queue */ public static final String TAG_QUEUE = "Queue"; @@ -378,7 +381,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR if (imageUrl != null) { return imageUrl; } else if (media != null && media.hasEmbeddedPicture()) { - return media.getLocalMediaUrl(); + return FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER + media.getLocalMediaUrl(); } else if (feed != null) { return feed.getImageLocation(); } else { @@ -481,6 +484,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, ImageR */ public void removeTag(String tag) { tags.remove(tag); } + @NonNull @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java index 719383d23..e8e478a86 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java @@ -1,8 +1,10 @@ package de.danoeh.antennapod.core.feed; import android.text.TextUtils; +import android.util.Log; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import de.danoeh.antennapod.core.storage.DBReader; @@ -11,17 +13,21 @@ import de.danoeh.antennapod.core.util.LongList; import static de.danoeh.antennapod.core.feed.FeedItem.TAG_FAVORITE; public class FeedItemFilter { + private final String[] mProperties; private boolean showPlayed = false; private boolean showUnplayed = false; private boolean showPaused = false; + private boolean showNotPaused = false; private boolean showQueued = false; private boolean showNotQueued = false; private boolean showDownloaded = false; private boolean showNotDownloaded = false; private boolean showHasMedia = false; + private boolean showNoMedia = false; private boolean showIsFavorite = false; + private boolean showNotFavorite = false; public FeedItemFilter(String properties) { this(TextUtils.split(properties, ",")); @@ -29,15 +35,18 @@ public class FeedItemFilter { public FeedItemFilter(String[] properties) { this.mProperties = properties; - for(String property : properties) { + for (String property : properties) { // see R.arrays.feed_filter_values - switch(property) { + switch (property) { case "unplayed": showUnplayed = true; break; case "paused": showPaused = true; break; + case "not_paused": + showNotPaused = true; + break; case "played": showPlayed = true; break; @@ -56,9 +65,17 @@ public class FeedItemFilter { case "has_media": showHasMedia = true; break; + case "no_media": + showNoMedia = true; + break; case "is_favorite": showIsFavorite = true; break; + case "not_favorite": + showNotFavorite = true; + break; + default: + break; } } } @@ -77,12 +94,15 @@ public class FeedItemFilter { if (showQueued && showNotQueued) return result; if (showDownloaded && showNotDownloaded) return result; - final LongList queuedIds = DBReader.getQueueIDList(); - for(FeedItem item : items) { + final LongList queuedIds = DBReader.getQueueIDList(); + for (FeedItem item : items) { // If the item does not meet a requirement, skip it. + if (showPlayed && !item.isPlayed()) continue; if (showUnplayed && item.isPlayed()) continue; + if (showPaused && item.getState() != FeedItem.State.IN_PROGRESS) continue; + if (showNotPaused && item.getState() == FeedItem.State.IN_PROGRESS) continue; boolean queued = queuedIds.contains(item.getId()); if (showQueued && !queued) continue; @@ -93,8 +113,10 @@ public class FeedItemFilter { if (showNotDownloaded && downloaded) continue; if (showHasMedia && !item.hasMedia()) continue; + if (showNoMedia && item.hasMedia()) continue; if (showIsFavorite && !item.isTagged(TAG_FAVORITE)) continue; + if (showNotFavorite && item.isTagged(TAG_FAVORITE)) continue; // If the item reaches here, it meets all criteria result.add(item); @@ -107,4 +129,8 @@ public class FeedItemFilter { return mProperties.clone(); } + public boolean isShowDownloaded() { + return showDownloaded; + } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java new file mode 100644 index 000000000..fcbe2e4ab --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilterGroup.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.feed; + +import de.danoeh.antennapod.core.R; + +public enum FeedItemFilterGroup { + PLAYED(new ItemProperties(R.string.hide_played_episodes_label, "played"), + new ItemProperties(R.string.not_played, "unplayed")), + PAUSED(new ItemProperties(R.string.hide_paused_episodes_label, "paused"), + new ItemProperties(R.string.not_paused, "not_paused")), + FAVORITE(new ItemProperties(R.string.hide_is_favorite_label, "is_favorite"), + new ItemProperties(R.string.not_favorite, "not_favorite")), + MEDIA(new ItemProperties(R.string.has_media, "has_media"), + new ItemProperties(R.string.no_media, "no_media")), + QUEUED(new ItemProperties(R.string.queued_label, "queued"), + new ItemProperties(R.string.not_queued_label, "not_queued")), + DOWNLOADED(new ItemProperties(R.string.hide_downloaded_episodes_label, "downloaded"), + new ItemProperties(R.string.hide_not_downloaded_episodes_label, "not_downloaded")); + + public final ItemProperties[] values; + + FeedItemFilterGroup(ItemProperties... values) { + this.values = values; + } + + public static class ItemProperties { + + public final int displayName; + public final String filterId; + + public ItemProperties(int displayName, String filterId) { + this.displayName = displayName; + this.filterId = filterId; + } + + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 7e1a5fd9b..88945b930 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -11,6 +11,7 @@ import androidx.annotation.Nullable; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.MediaDescriptionCompat; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.concurrent.Callable; @@ -32,6 +33,7 @@ public class FeedMedia extends FeedFile implements Playable { public static final int FEEDFILETYPE_FEEDMEDIA = 2; public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; + public static final String FILENAME_PREFIX_EMBEDDED_COVER = "metadata-retriever:"; public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; private static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; @@ -375,26 +377,37 @@ public class FeedMedia extends FeedFile implements Playable { } @Override - public void loadChapterMarks() { + public void loadChapterMarks(Context context) { if (item == null && itemID != 0) { item = DBReader.getFeedItem(itemID); } if (item == null || item.getChapters() != null) { return; } - // check if chapters are stored in db and not loaded yet. + + List<Chapter> chapters = loadChapters(context); + if (chapters == null) { + // Do not try loading again. There are no chapters. + item.setChapters(Collections.emptyList()); + } else { + item.setChapters(chapters); + } + } + + private List<Chapter> loadChapters(Context context) { + List<Chapter> chaptersFromDatabase = null; if (item.hasChapters()) { - DBReader.loadChaptersOfFeedItem(item); + chaptersFromDatabase = DBReader.loadChaptersOfFeedItem(item); + } + + List<Chapter> chaptersFromMediaFile; + if (localFileAvailable()) { + chaptersFromMediaFile = ChapterUtils.loadChaptersFromFileUrl(this); } else { - if(localFileAvailable()) { - ChapterUtils.loadChaptersFromFileUrl(this); - } else { - ChapterUtils.loadChaptersFromStreamUrl(this); - } - if (item.getChapters() != null) { - DBWriter.setFeedItem(item); - } + chaptersFromMediaFile = ChapterUtils.loadChaptersFromStreamUrl(this, context); } + + return ChapterMerger.merge(chaptersFromDatabase, chaptersFromMediaFile); } @Override @@ -481,7 +494,7 @@ public class FeedMedia extends FeedFile implements Playable { @Override public void onPlaybackStart() { - startPosition = (position > 0) ? position : 0; + startPosition = Math.max(position, 0); playedDurationWhenStarted = played_duration; } @@ -557,7 +570,7 @@ public class FeedMedia extends FeedFile implements Playable { if (item != null) { return item.getImageLocation(); } else if (hasEmbeddedPicture()) { - return getLocalMediaUrl(); + return FILENAME_PREFIX_EMBEDDED_COVER + getLocalMediaUrl(); } else { return null; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java index 2a2568f28..5ffee0d62 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -38,11 +38,8 @@ public class FeedPreferences { private int feedSkipEnding; public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password) { - this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting, username, password, new FeedFilter(), SPEED_USE_GLOBAL); - } - - private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed) { - this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting, username, password, new FeedFilter(), feedPlaybackSpeed, 0, 0); + this(feedID, autoDownload, true, auto_delete_action, volumeAdaptionSetting, + username, password, new FeedFilter(), SPEED_USE_GLOBAL, 0, 0); } private FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, VolumeAdaptionSetting volumeAdaptionSetting, String username, String password, @NonNull FeedFilter filter, float feedPlaybackSpeed, int feedSkipIntro, int feedSkipEnding) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java new file mode 100644 index 000000000..2791be08c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -0,0 +1,162 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.ContentResolver; +import android.content.Context; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.text.TextUtils; + +import androidx.documentfile.provider.DocumentFile; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.DownloadError; + +public class LocalFeedUpdater { + + public static void updateFeed(Feed feed, Context context) { + String uriString = feed.getDownload_url().replace(Feed.PREFIX_LOCAL_FOLDER, ""); + DocumentFile documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString)); + if (documentFolder == null) { + reportError(feed, "Unable to retrieve document tree." + + "Try re-connecting the folder on the podcast info page."); + return; + } + if (!documentFolder.exists() || !documentFolder.canRead()) { + reportError(feed, "Cannot read local directory. Try re-connecting the folder on the podcast info page."); + return; + } + + if (feed.getItems() == null) { + feed.setItems(new ArrayList<>()); + } + //make sure it is the latest 'version' of this feed from the db (all items etc) + feed = DBTasks.updateFeed(context, feed, false); + + // list files in feed folder + List<DocumentFile> mediaFiles = new ArrayList<>(); + Set<String> mediaFileNames = new HashSet<>(); + for (DocumentFile file : documentFolder.listFiles()) { + String mime = file.getType(); + if (mime != null && (mime.startsWith("audio/") || mime.startsWith("video/"))) { + mediaFiles.add(file); + mediaFileNames.add(file.getName()); + } + } + + // add new files to feed and update item data + List<FeedItem> newItems = feed.getItems(); + for (DocumentFile f : mediaFiles) { + FeedItem oldItem = feedContainsFile(feed, f.getName()); + FeedItem newItem = createFeedItem(feed, f, context); + if (oldItem == null) { + newItems.add(newItem); + } else { + oldItem.updateFromOther(newItem); + } + } + + // remove feed items without corresponding file + Iterator<FeedItem> it = newItems.iterator(); + while (it.hasNext()) { + FeedItem feedItem = it.next(); + if (!mediaFileNames.contains(feedItem.getLink())) { + it.remove(); + } + } + + List<String> iconLocations = Arrays.asList("folder.jpg", "Folder.jpg", "folder.png", "Folder.png"); + for (String iconLocation : iconLocations) { + DocumentFile image = documentFolder.findFile(iconLocation); + if (image != null) { + feed.setImageUrl(image.getUri().toString()); + break; + } + } + if (StringUtils.isBlank(feed.getImageUrl())) { + // set default feed image + feed.setImageUrl(getDefaultIconUrl(context)); + } + if (feed.getPreferences().getAutoDownload()) { + feed.getPreferences().setAutoDownload(false); + feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); + try { + DBWriter.setFeedPreferences(feed.getPreferences()).get(); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + } + + // update items, delete items without existing file; + // only delete items if the folder contains at least one element to avoid accidentally + // deleting played state or position in case the folder is temporarily unavailable. + boolean removeUnlistedItems = (newItems.size() >= 1); + DBTasks.updateFeed(context, feed, removeUnlistedItems); + } + + /** + * Returns the URL of the default icon for a local feed. The URL refers to an app resource file. + */ + public static String getDefaultIconUrl(Context context) { + String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon); + return ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + + context.getPackageName() + "/raw/" + + resourceEntryName; + } + + private static FeedItem feedContainsFile(Feed feed, String filename) { + List<FeedItem> items = feed.getItems(); + for (FeedItem i : items) { + if (i.getMedia() != null && i.getLink().equals(filename)) { + return i; + } + } + return null; + } + + private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) { + String uuid = UUID.randomUUID().toString(); + FeedItem item = new FeedItem(0, file.getName(), uuid, file.getName(), new Date(), + FeedItem.UNPLAYED, feed); + item.setAutoDownload(false); + + MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); + mediaMetadataRetriever.setDataSource(context, file.getUri()); + String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + String title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); + if (!TextUtils.isEmpty(title)) { + item.setTitle(title); + } + + //add the media to the item + long duration = Long.parseLong(durationStr); + long size = file.length(); + FeedMedia media = new FeedMedia(0, item, (int) duration, 0, size, file.getType(), + file.getUri().toString(), file.getUri().toString(), false, null, 0, 0); + media.setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null); + item.setMedia(media); + + return item; + } + + private static void reportError(Feed feed, String reasonDetailed) { + DownloadStatus status = new DownloadStatus(feed, feed.getTitle(), + DownloadError.ERROR_IO_ERROR, false, reasonDetailed, true); + DBWriter.addDownloadStatus(status); + DBWriter.setFeedLastUpdateFailed(feed.getId(), true); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java new file mode 100644 index 000000000..93f098ecf --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilter.java @@ -0,0 +1,106 @@ +package de.danoeh.antennapod.core.feed; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.core.util.LongIntMap; + +public class SubscriptionsFilter { + private static final String divider = ","; + + private final String[] properties; + + private boolean showIfCounterGreaterZero = false; + + private boolean showAutoDownloadEnabled = false; + private boolean showAutoDownloadDisabled = false; + + private boolean showUpdatedEnabled = false; + private boolean showUpdatedDisabled = false; + + public SubscriptionsFilter(String properties) { + this(TextUtils.split(properties, divider)); + } + + + public SubscriptionsFilter(String[] properties) { + this.properties = properties; + for (String property : properties) { + // see R.arrays.feed_filter_values + switch (property) { + case "counter_greater_zero": + showIfCounterGreaterZero = true; + break; + case "enabled_auto_download": + showAutoDownloadEnabled = true; + break; + case "disabled_auto_download": + showAutoDownloadDisabled = true; + break; + case "enabled_updates": + showUpdatedEnabled = true; + break; + case "disabled_updates": + showUpdatedDisabled = true; + break; + default: + break; + } + } + } + + public boolean isEnabled() { + return properties.length > 0; + } + + /** + * Run a list of feed items through the filter. + */ + public List<Feed> filter(List<Feed> items, LongIntMap feedCounters) { + if (properties.length == 0) { + return items; + } + + List<Feed> result = new ArrayList<>(); + + for (Feed item : items) { + FeedPreferences itemPreferences = item.getPreferences(); + + // If the item does not meet a requirement, skip it. + if (showAutoDownloadEnabled && !itemPreferences.getAutoDownload()) { + continue; + } else if (showAutoDownloadDisabled && itemPreferences.getAutoDownload()) { + continue; + } + + if (showUpdatedEnabled && !itemPreferences.getKeepUpdated()) { + continue; + } else if (showUpdatedDisabled && itemPreferences.getKeepUpdated()) { + continue; + } + + // If the item reaches here, it meets all criteria (except counter > 0) + result.add(item); + } + + if (showIfCounterGreaterZero) { + for (int i = result.size() - 1; i >= 0; i--) { + if (feedCounters.get(result.get(i).getId()) <= 0) { + result.remove(i); + } + } + } + + return result; + } + + public String[] getValues() { + return properties.clone(); + } + + public String serialize() { + return TextUtils.join(divider, getValues()); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java new file mode 100644 index 000000000..7db0456a0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SubscriptionsFilterGroup.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.core.feed; + +import de.danoeh.antennapod.core.R; + +public enum SubscriptionsFilterGroup { + COUNTER_GREATER_ZERO(new ItemProperties(R.string.subscriptions_counter_greater_zero, "counter_greater_zero")), + AUTO_DOWNLOAD(new ItemProperties(R.string.auto_downloaded, "enabled_auto_download"), + new ItemProperties(R.string.not_auto_downloaded, "disabled_auto_download")), + UPDATED(new ItemProperties(R.string.kept_updated, "enabled_updates"), + new ItemProperties(R.string.not_kept_updated, "disabled_updates")); + + + public final ItemProperties[] values; + + SubscriptionsFilterGroup(ItemProperties... values) { + this.values = values; + } + + public static class ItemProperties { + + public final int displayName; + public final String filterId; + + public ItemProperties(int displayName, String filterId) { + this.displayName = displayName; + this.filterId = filterId; + } + + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java index 50511526f..ab4247cef 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java @@ -9,6 +9,8 @@ import com.bumptech.glide.Registry; import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; +import com.bumptech.glide.load.model.StringLoader; +import com.bumptech.glide.load.model.UriLoader; import com.bumptech.glide.module.AppGlideModule; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; @@ -33,7 +35,10 @@ public class ApGlideModule extends AppGlideModule { @Override public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) { - registry.replace(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory()); + registry.replace(String.class, InputStream.class, new MetadataRetrieverLoader.Factory(context)); + registry.append(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory()); + registry.append(String.class, InputStream.class, new StringLoader.StreamFactory()); + registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java index 071b1d0c9..8c80e9151 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.glide; +import android.content.ContentResolver; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -22,7 +23,7 @@ import java.io.IOException; import java.io.InputStream; /** - * @see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader + * {@see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader}. */ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { @@ -52,14 +53,7 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { * Constructor for a new Factory that runs requests using a static singleton client. */ Factory() { - this(getInternalClient()); - } - - /** - * Constructor for a new Factory that runs requests using given client. - */ - Factory(OkHttpClient client) { - this.client = client; + this.client = getInternalClient(); } @NonNull @@ -83,19 +77,15 @@ class ApOkHttpUrlLoader implements ModelLoader<String, InputStream> { @Nullable @Override public LoadData<InputStream> buildLoadData(@NonNull String model, int width, int height, @NonNull Options options) { - if (TextUtils.isEmpty(model)) { - return null; - } else if (model.startsWith("/")) { - return new LoadData<>(new ObjectKey(model), new AudioCoverFetcher(model)); - } else { - GlideUrl url = new GlideUrl(model); - return new LoadData<>(new ObjectKey(model), new OkHttpStreamFetcher(client, url)); - } + return new LoadData<>(new ObjectKey(model), new OkHttpStreamFetcher(client, new GlideUrl(model))); } @Override - public boolean handles(@NonNull String s) { - return true; + public boolean handles(@NonNull String model) { + // Leave content URIs to Glide's default loaders + return !TextUtils.isEmpty(model) + && !model.startsWith(ContentResolver.SCHEME_CONTENT) + && !model.startsWith(ContentResolver.SCHEME_ANDROID_RESOURCE); } private static class NetworkAllowanceInterceptor implements Interceptor { diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java index 6a237573b..b6b607904 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/AudioCoverFetcher.java @@ -1,7 +1,10 @@ package de.danoeh.antennapod.core.glide; +import android.content.ContentResolver; +import android.content.Context; import android.media.MediaMetadataRetriever; +import android.net.Uri; import androidx.annotation.NonNull; import com.bumptech.glide.Priority; import com.bumptech.glide.load.DataSource; @@ -17,16 +20,22 @@ class AudioCoverFetcher implements DataFetcher<InputStream> { private static final String TAG = "AudioCoverFetcher"; private final String path; + private final Context context; - public AudioCoverFetcher(String path) { + public AudioCoverFetcher(String path, Context context) { this.path = path; + this.context = context; } @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) { MediaMetadataRetriever retriever = new MediaMetadataRetriever(); try { - retriever.setDataSource(path); + if (path.startsWith(ContentResolver.SCHEME_CONTENT)) { + retriever.setDataSource(context, Uri.parse(path)); + } else { + retriever.setDataSource(path); + } byte[] picture = retriever.getEmbeddedPicture(); if (picture != null) { callback.onDataReady(new ByteArrayInputStream(picture)); @@ -41,6 +50,7 @@ class AudioCoverFetcher implements DataFetcher<InputStream> { @Override public void cleanup() { // nothing to clean up } + @Override public void cancel() { // cannot cancel } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java index 36da11eca..35a9d987b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java @@ -28,8 +28,9 @@ import org.apache.commons.io.IOUtils; public final class ChapterImageModelLoader implements ModelLoader<EmbeddedChapterImage, ByteBuffer> { public static class Factory implements ModelLoaderFactory<EmbeddedChapterImage, ByteBuffer> { + @NonNull @Override - public ModelLoader<EmbeddedChapterImage, ByteBuffer> build(MultiModelLoaderFactory unused) { + public ModelLoader<EmbeddedChapterImage, ByteBuffer> build(@NonNull MultiModelLoaderFactory unused) { return new ChapterImageModelLoader(); } @@ -41,12 +42,15 @@ public final class ChapterImageModelLoader implements ModelLoader<EmbeddedChapte @Nullable @Override - public LoadData<ByteBuffer> buildLoadData(EmbeddedChapterImage model, int width, int height, Options options) { + public LoadData<ByteBuffer> buildLoadData(@NonNull EmbeddedChapterImage model, + int width, + int height, + @NonNull Options options) { return new LoadData<>(new ObjectKey(model), new EmbeddedImageFetcher(model)); } @Override - public boolean handles(EmbeddedChapterImage model) { + public boolean handles(@NonNull EmbeddedChapterImage model) { return true; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java b/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java index d0301db2f..1f8ae5ad9 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java @@ -21,7 +21,10 @@ public class FastBlurTransformation extends BitmapTransformation { } @Override - protected Bitmap transform(BitmapPool pool, Bitmap source, int outWidth, int outHeight) { + protected Bitmap transform(@NonNull BitmapPool pool, + @NonNull Bitmap source, + int outWidth, + int outHeight) { int targetWidth = outWidth / 3; int targetHeight = (int) (1.0 * outHeight * targetWidth / outWidth); Bitmap resized = ThumbnailUtils.extractThumbnail(source, targetWidth, targetHeight); diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java new file mode 100644 index 000000000..baa06e722 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/MetadataRetrieverLoader.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.core.glide; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.bumptech.glide.load.model.MultiModelLoaderFactory; +import com.bumptech.glide.signature.ObjectKey; +import de.danoeh.antennapod.core.feed.FeedMedia; + +import java.io.InputStream; + +class MetadataRetrieverLoader implements ModelLoader<String, InputStream> { + + /** + * The default factory for {@link MetadataRetrieverLoader}s. + */ + public static class Factory implements ModelLoaderFactory<String, InputStream> { + private final Context context; + + Factory(Context context) { + this.context = context; + } + + @NonNull + @Override + public ModelLoader<String, InputStream> build(@NonNull MultiModelLoaderFactory multiFactory) { + return new MetadataRetrieverLoader(context); + } + + @Override + public void teardown() { + // Do nothing, this instance doesn't own the client. + } + } + + private final Context context; + + private MetadataRetrieverLoader(Context context) { + this.context = context; + } + + @Nullable + @Override + public LoadData<InputStream> buildLoadData(@NonNull String model, + int width, int height, @NonNull Options options) { + return new LoadData<>(new ObjectKey(model), + new AudioCoverFetcher(model.replace(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER, ""), context)); + } + + @Override + public boolean handles(@NonNull String model) { + return model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java index a4612d857..08ea27434 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.preferences; import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import de.danoeh.antennapod.core.event.PlayerStatusEvent; diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index 876251563..56dd95fe6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Build; -import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; @@ -12,6 +11,7 @@ import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationCompat; +import androidx.preference.PreferenceManager; import org.json.JSONArray; import org.json.JSONException; @@ -19,15 +19,20 @@ import org.json.JSONException; import java.io.File; import java.io.IOException; import java.net.Proxy; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.service.download.ProxyConfig; import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; @@ -46,14 +51,12 @@ import de.danoeh.antennapod.core.util.download.AutoUpdateManager; public class UserPreferences { private UserPreferences(){} - private static final String IMPORT_DIR = "import/"; - private static final String TAG = "UserPreferences"; // User Interface public static final String PREF_THEME = "prefTheme"; public static final String PREF_HIDDEN_DRAWER_ITEMS = "prefHiddenDrawerItems"; - private static final String PREF_DRAWER_FEED_ORDER = "prefDrawerFeedOrder"; + public static final String PREF_DRAWER_FEED_ORDER = "prefDrawerFeedOrder"; private static final String PREF_DRAWER_FEED_COUNTER = "prefDrawerFeedIndicator"; public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; public static final String PREF_USE_EPISODE_COVER = "prefEpisodeCover"; @@ -64,6 +67,7 @@ public class UserPreferences { private static final String PREF_SHOW_AUTO_DOWNLOAD_REPORT = "prefShowAutoDownloadReport"; public static final String PREF_BACK_BUTTON_BEHAVIOR = "prefBackButtonBehavior"; private static final String PREF_BACK_BUTTON_GO_TO_PAGE = "prefBackButtonGoToPage"; + public static final String PREF_FILTER_FEED = "prefSubscriptionsFilter"; public static final String PREF_QUEUE_KEEP_SORTED = "prefQueueKeepSorted"; public static final String PREF_QUEUE_KEEP_SORTED_ORDER = "prefQueueKeepSortedOrder"; @@ -111,6 +115,7 @@ public class UserPreferences { private static final String PREF_DATA_FOLDER = "prefDataFolder"; public static final String PREF_IMAGE_CACHE_SIZE = "prefImageCacheSize"; public static final String PREF_DELETE_REMOVES_FROM_QUEUE = "prefDeleteRemovesFromQueue"; + public static final String PREF_USAGE_COUNTING_DATE = "prefUsageCounting"; // Mediaplayer public static final String PREF_MEDIA_PLAYER = "prefMediaPlayer"; @@ -161,7 +166,6 @@ public class UserPreferences { UserPreferences.context = context.getApplicationContext(); UserPreferences.prefs = PreferenceManager.getDefaultSharedPreferences(context); - createImportDirectory(); createNoMediaFile(); } @@ -242,6 +246,12 @@ public class UserPreferences { return Integer.parseInt(value); } + public static void setFeedOrder(String selected) { + prefs.edit() + .putString(PREF_DRAWER_FEED_ORDER, selected) + .apply(); + } + public static int getFeedCounterSetting() { String value = prefs.getString(PREF_DRAWER_FEED_COUNTER, "" + FEED_COUNTER_SHOW_NEW); return Integer.parseInt(value); @@ -414,7 +424,7 @@ public class UserPreferences { return prefs.getBoolean(PREF_PLAYBACK_SKIP_SILENCE, false); } - public static float[] getPlaybackSpeedArray() { + public static List<Float> getPlaybackSpeedArray() { return readPlaybackSpeedArray(prefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null)); } @@ -628,8 +638,7 @@ public class UserPreferences { } public static boolean isQueueLocked() { - return prefs.getBoolean(PREF_QUEUE_LOCKED, false) - || isQueueKeepSorted(); + return prefs.getBoolean(PREF_QUEUE_LOCKED, false); } public static void setFastForwardSecs(int secs) { @@ -662,10 +671,13 @@ public class UserPreferences { .apply(); } - public static void setPlaybackSpeedArray(String[] speeds) { + public static void setPlaybackSpeedArray(List<Float> speeds) { + DecimalFormatSymbols format = new DecimalFormatSymbols(Locale.US); + format.setDecimalSeparator('.'); + DecimalFormat speedFormat = new DecimalFormat("0.00", format); JSONArray jsonArray = new JSONArray(); - for (String speed : speeds) { - jsonArray.put(speed); + for (float speed : speeds) { + jsonArray.put(speedFormat.format(speed)); } prefs.edit() .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()) @@ -775,13 +787,13 @@ public class UserPreferences { } } - private static float[] readPlaybackSpeedArray(String valueFromPrefs) { + private static List<Float> readPlaybackSpeedArray(String valueFromPrefs) { if (valueFromPrefs != null) { try { JSONArray jsonArray = new JSONArray(valueFromPrefs); - float[] selectedSpeeds = new float[jsonArray.length()]; + List<Float> selectedSpeeds = new ArrayList<>(); for (int i = 0; i < jsonArray.length(); i++) { - selectedSpeeds[i] = (float) jsonArray.getDouble(i); + selectedSpeeds.add((float) jsonArray.getDouble(i)); } return selectedSpeeds; } catch (JSONException e) { @@ -790,7 +802,7 @@ public class UserPreferences { } } // If this preference hasn't been set yet, return the default options - return new float[] { 0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f }; + return Arrays.asList(0.75f, 1.0f, 1.25f, 1.5f, 1.75f, 2.0f); } public static String getMediaPlayer() { @@ -826,8 +838,8 @@ public class UserPreferences { public static VideoBackgroundBehavior getVideoBackgroundBehavior() { switch (prefs.getString(PREF_VIDEO_BEHAVIOR, "pip")) { case "stop": return VideoBackgroundBehavior.STOP; - case "pip": return VideoBackgroundBehavior.PICTURE_IN_PICTURE; case "continue": return VideoBackgroundBehavior.CONTINUE_PLAYING; + case "pip": //Deliberate fall-through default: return VideoBackgroundBehavior.PICTURE_IN_PICTURE; } } @@ -914,7 +926,6 @@ public class UserPreferences { prefs.edit() .putString(PREF_DATA_FOLDER, dir) .apply(); - createImportDirectory(); } /** @@ -934,24 +945,6 @@ public class UserPreferences { } /** - * Creates the import directory if it doesn't exist and if storage is - * available - */ - private static void createImportDirectory() { - File importDir = getDataFolder(IMPORT_DIR); - if (importDir != null) { - if (importDir.exists()) { - Log.d(TAG, "Import directory already exists"); - } else { - Log.d(TAG, "Creating import directory"); - importDir.mkdir(); - } - } else { - Log.d(TAG, "Could not access external storage."); - } - } - - /** * * @return true if auto update is set to a specific time * false if auto update is set to interval @@ -977,11 +970,11 @@ public class UserPreferences { public static BackButtonBehavior getBackButtonBehavior() { switch (prefs.getString(PREF_BACK_BUTTON_BEHAVIOR, "default")) { - case "default": return BackButtonBehavior.DEFAULT; case "drawer": return BackButtonBehavior.OPEN_DRAWER; case "doubletap": return BackButtonBehavior.DOUBLE_TAP; case "prompt": return BackButtonBehavior.SHOW_PROMPT; case "page": return BackButtonBehavior.GO_TO_PAGE; + case "default": // Deliberate fall-through default: return BackButtonBehavior.DEFAULT; } } @@ -1052,4 +1045,31 @@ public class UserPreferences { .putString(PREF_QUEUE_KEEP_SORTED_ORDER, sortOrder.name()) .apply(); } + + public static SubscriptionsFilter getSubscriptionsFilter() { + String value = prefs.getString(PREF_FILTER_FEED, ""); + return new SubscriptionsFilter(value); + } + + public static void setSubscriptionsFilter(SubscriptionsFilter value) { + prefs.edit() + .putString(PREF_FILTER_FEED, value.serialize()) + .apply(); + } + + public static long getUsageCountingDateMillis() { + return prefs.getLong(PREF_USAGE_COUNTING_DATE, -1); + } + + private static void setUsageCountingDateMillis(long value) { + prefs.edit().putLong(PREF_USAGE_COUNTING_DATE, value).apply(); + } + + public static void resetUsageCountingDate() { + setUsageCountingDateMillis(Calendar.getInstance().getTimeInMillis()); + } + + public static void unsetUsageCountingDate() { + setUsageCountingDateMillis(-1); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java index b683f849c..abee9d8d3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java @@ -15,6 +15,7 @@ public class MediaButtonReceiver extends BroadcastReceiver { private static final String TAG = "MediaButtonReceiver"; public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.KEYCODE"; public static final String EXTRA_SOURCE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.SOURCE"; + public static final String EXTRA_HARDWAREBUTTON = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.HARDWAREBUTTON"; public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER"; @@ -30,6 +31,12 @@ public class MediaButtonReceiver extends BroadcastReceiver { Intent serviceIntent = new Intent(context, PlaybackService.class); serviceIntent.putExtra(EXTRA_KEYCODE, event.getKeyCode()); serviceIntent.putExtra(EXTRA_SOURCE, event.getSource()); + //detect if this is a hardware button press + if (event.getEventTime() > 0 || event.getDownTime() > 0) { + serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, true); + } else { + serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, false); + } ContextCompat.startForegroundService(context, serviceIntent); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java index 7bf1a5df1..74735a264 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java @@ -121,7 +121,7 @@ public class PlayerWidgetJobService extends SafeJobIntentService { views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer); try { - Bitmap icon = null; + Bitmap icon; int iconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); icon = Glide.with(PlayerWidgetJobService.this) .asBitmap() diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java index 78c4d3f48..3f503c6b4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java @@ -115,7 +115,7 @@ public class DownloadRequest implements Parcelable { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || !(o instanceof DownloadRequest)) return false; + if (!(o instanceof DownloadRequest)) return false; DownloadRequest that = (DownloadRequest) o; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index e44aa716a..f1b35fe23 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -10,6 +10,7 @@ import android.content.IntentFilter; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.text.TextUtils; import android.util.Log; @@ -178,7 +179,7 @@ public class DownloadService extends Service { public void onCreate() { Log.d(TAG, "Service started"); isRunning = true; - handler = new Handler(); + handler = new Handler(Looper.getMainLooper()); notificationManager = new DownloadServiceNotification(this); IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java index 64666d25d..975bc3cb3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java @@ -153,7 +153,11 @@ public class DownloadServiceNotification { iconId = R.drawable.ic_notification_sync_error; intent = ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(context); id = R.id.notification_download_report; - content = String.format(context.getString(R.string.download_report_content), successfulDownloads, failedDownloads); + content = context.getResources() + .getQuantityString(R.plurals.download_report_content, + successfulDownloads, + successfulDownloads, + failedDownloads); } NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index ef86c9024..65b7ed7d1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -191,7 +191,7 @@ public class HttpDownloader extends Downloader { } byte[] buffer = new byte[BUFFER_SIZE]; - int count = 0; + int count; request.setStatusMsg(R.string.download_running); Log.d(TAG, "Getting size of download"); request.setSize(responseBody.contentLength() + request.getSoFar()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java index c50162788..18c5fce27 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedParserTask.java @@ -18,7 +18,6 @@ import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.IOException; -import java.util.Date; import java.util.concurrent.Callable; public class FeedParserTask implements Callable<FeedHandlerResult> { @@ -104,13 +103,6 @@ public class FeedParserTask implements Callable<FeedHandlerResult> { if (item.getTitle() == null) { throw new InvalidFeedException("Item has no title: " + item); } - if (item.getPubDate() == null) { - Log.e(TAG, "Item has no pubDate. Using current time as pubDate"); - if (item.getTitle() != null) { - Log.e(TAG, "Title of invalid item: " + item.getTitle()); - } - item.setPubDate(new Date()); - } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java index 8be3d2980..483a2aa56 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FeedSyncTask.java @@ -30,8 +30,7 @@ public class FeedSyncTask { return false; } - Feed[] savedFeeds = DBTasks.updateFeed(context, result.feed); - Feed savedFeed = savedFeeds[0]; + Feed savedFeed = DBTasks.updateFeed(context, result.feed, false); // If loadAllPages=true, check if another page is available and queue it for download final boolean loadAllPages = request.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES); final Feed feed = result.feed; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index 9e2b69810..501214399 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -56,7 +56,7 @@ public class MediaDownloadedHandler implements Runnable { // check if file has chapters if (media.getItem() != null && !media.getItem().hasChapters()) { - ChapterUtils.loadChaptersFromFileUrl(media); + media.setChapters(ChapterUtils.loadChaptersFromFileUrl(media)); } // Get duration diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java index dddf442f3..71bbf2efd 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java @@ -8,7 +8,6 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.DefaultLoadControl; import com.google.android.exoplayer2.DefaultRenderersFactory; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; @@ -76,9 +75,11 @@ public class ExoPlayerWrapper implements IPlayer { DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS); loadControl.setBackBuffer(UserPreferences.getRewindSecs() * 1000 + 500, true); - trackSelector = new DefaultTrackSelector(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultRenderersFactory(context), - trackSelector, loadControl.createDefaultLoadControl()); + trackSelector = new DefaultTrackSelector(context); + exoPlayer = new SimpleExoPlayer.Builder(context, new DefaultRenderersFactory(context)) + .setTrackSelector(trackSelector) + .setLoadControl(loadControl.createDefaultLoadControl()) + .build(); exoPlayer.setSeekParameters(SeekParameters.EXACT); exoPlayer.addListener(new Player.EventListener() { @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index fbdc9a52b..ae5d62872 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -2,9 +2,7 @@ package de.danoeh.antennapod.core.service.playback; import android.content.Context; import android.media.AudioAttributes; -import android.media.AudioFocusRequest; import android.media.AudioManager; -import android.os.Build; import android.os.PowerManager; import androidx.annotation.NonNull; import android.telephony.TelephonyManager; @@ -12,11 +10,13 @@ import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; +import androidx.media.AudioAttributesCompat; +import androidx.media.AudioFocusRequestCompat; +import androidx.media.AudioManagerCompat; import org.antennapod.audio.MediaPlayer; import java.io.File; import java.io.IOException; -import java.util.EnumSet; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; @@ -57,6 +57,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private final AtomicBoolean startWhenPrepared; private volatile boolean pausedBecauseOfTransientAudiofocusLoss; private volatile Pair<Integer, Integer> videoSize; + private final AudioFocusRequestCompat audioFocusRequest; /** * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads @@ -154,6 +155,16 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { pausedBecauseOfTransientAudiofocusLoss = false; mediaType = MediaType.UNKNOWN; videoSize = null; + + AudioAttributesCompat audioAttributes = new AudioAttributesCompat.Builder() + .setUsage(AudioAttributesCompat.USAGE_MEDIA) + .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH) + .build(); + audioFocusRequest = new AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) + .setAudioAttributes(audioAttributes) + .setOnAudioFocusChangeListener(audioFocusChangeListener) + .setWillPauseWhenDucked(true) + .build(); } /** @@ -287,25 +298,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { private void resumeSync() { if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - int focusGained; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AudioAttributes audioAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_MEDIA) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build(); - AudioFocusRequest audioFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) - .setAudioAttributes(audioAttributes) - .setOnAudioFocusChangeListener(audioFocusChangeListener) - .setAcceptsDelayedFocusGain(true) - .setWillPauseWhenDucked(true) - .build(); - focusGained = audioManager.requestAudioFocus(audioFocusRequest); - } else { - focusGained = audioManager.requestAudioFocus( - audioFocusChangeListener, AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); - } + int focusGained = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest); if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { Log.d(TAG, "Audiofocus successfully requested"); @@ -373,13 +366,7 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { } private void abandonAudioFocus() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - AudioFocusRequest.Builder builder = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN) - .setOnAudioFocusChangeListener(audioFocusChangeListener); - audioManager.abandonAudioFocusRequest(builder.build()); - } else { - audioManager.abandonAudioFocus(audioFocusChangeListener); - } + AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequest); } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index e9c8e1bbb..60075dda6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.service.playback; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.app.UiModeManager; import android.bluetooth.BluetoothA2dp; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -10,6 +11,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.media.AudioManager; import android.media.MediaPlayer; @@ -19,7 +21,7 @@ import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Vibrator; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; @@ -35,6 +37,7 @@ import android.util.Log; import android.util.Pair; import android.view.KeyEvent; import android.view.SurfaceHolder; +import android.webkit.URLUtil; import android.widget.Toast; import com.bumptech.glide.Glide; @@ -82,6 +85,7 @@ import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -313,7 +317,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { } } emitter.onSuccess(queueItems); - }).subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(queueItems -> mediaSession.setQueue(queueItems), Throwable::printStackTrace); flavorHelper.initializeMediaPlayer(PlaybackService.this); mediaSession.setActive(true); @@ -419,7 +426,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { e.printStackTrace(); } } else if (parentId.startsWith("FeedId:")) { - Long feedId = Long.parseLong(parentId.split(":")[1]); + long feedId = Long.parseLong(parentId.split(":")[1]); List<FeedItem> feedItems = DBReader.getFeedItemList(DBReader.getFeed(feedId)); for (FeedItem feedItem : feedItems) { if (feedItem.getMedia() != null && feedItem.getMedia().getMediaItem() != null) { @@ -450,6 +457,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { notificationManager.cancel(R.id.notification_streaming_confirmation); final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); + final boolean hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false); final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false); Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); if (keycode == -1 && playable == null && !castDisconnect) { @@ -463,8 +471,15 @@ public class PlaybackService extends MediaBrowserServiceCompat { stateManager.stopForeground(true); } else { if (keycode != -1) { - Log.d(TAG, "Received media button event"); - boolean handled = handleKeycode(keycode, true); + boolean notificationButton; + if (hardwareButton) { + Log.d(TAG, "Received hardware button event"); + notificationButton = false; + } else { + Log.d(TAG, "Received media button event"); + notificationButton = true; + } + boolean handled = handleKeycode(keycode, notificationButton); if (!handled && !stateManager.hasReceivedValidStartCommand()) { stateManager.stopService(); return Service.START_NOT_STICKY; @@ -482,22 +497,34 @@ public class PlaybackService extends MediaBrowserServiceCompat { if (allowStreamAlways) { UserPreferences.setAllowMobileStreaming(true); } - if (stream && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime) { + boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl()); + if (stream && !NetworkUtils.isStreamingAllowed() && !allowStreamThisTime && !localFeed) { displayStreamingNotAllowedNotification(intent); PlaybackPreferences.writeNoMediaPlaying(); stateManager.stopService(); return Service.START_NOT_STICKY; } - if (playable instanceof FeedMedia) { - playable = DBReader.getFeedMedia(((FeedMedia) playable).getId()); - } - if (playable == null) { - Log.d(TAG, "Playable was not found. Stopping service."); - stateManager.stopService(); - return Service.START_NOT_STICKY; - } - mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); - addPlayableToQueue(playable); + + Observable.fromCallable( + () -> { + if (playable instanceof FeedMedia) { + return DBReader.getFeedMedia(((FeedMedia) playable).getId()); + } else { + return playable; + } + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + playableLoaded -> { + mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, + prepareImmediately); + addPlayableToQueue(playable); + }, error -> { + Log.d(TAG, "Playable was not found. Stopping service."); + stateManager.stopService(); + }); + return Service.START_NOT_STICKY; } else { Log.d(TAG, "Did not handle intent to PlaybackService: " + intent); Log.d(TAG, "Extras: " + intent.getExtras()); @@ -566,7 +593,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(pendingIntentAllowThisTime) .addAction(R.drawable.ic_stream_white, - getString(R.string.stream_label), + getString(R.string.confirm_mobile_streaming_button_once), pendingIntentAllowThisTime) .addAction(R.drawable.ic_stream_white, getString(R.string.confirm_mobile_streaming_button_always), @@ -677,26 +704,33 @@ public class PlaybackService extends MediaBrowserServiceCompat { } private void startPlayingFromPreferences() { - Playable playable = Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext()); - if (playable != null) { - if (PlaybackPreferences.getCurrentEpisodeIsStream() && !NetworkUtils.isStreamingAllowed()) { - displayStreamingNotAllowedNotification( - new PlaybackServiceStarter(this, playable) - .prepareImmediately(true) - .startWhenPrepared(true) - .shouldStream(true) - .getIntent()); - PlaybackPreferences.writeNoMediaPlaying(); - stateManager.stopService(); - return; - } - mediaPlayer.playMediaObject(playable, PlaybackPreferences.getCurrentEpisodeIsStream(), true, true); - stateManager.validStartCommandWasReceived(); - PlaybackService.this.updateMediaSessionMetadata(playable); - addPlayableToQueue(playable); - } else { - stateManager.stopService(); - } + Observable.fromCallable(() -> Playable.PlayableUtils.createInstanceFromPreferences(getApplicationContext())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + playable -> { + boolean localFeed = URLUtil.isContentUrl(playable.getStreamUrl()); + if (PlaybackPreferences.getCurrentEpisodeIsStream() + && !NetworkUtils.isStreamingAllowed() && !localFeed) { + displayStreamingNotAllowedNotification( + new PlaybackServiceStarter(this, playable) + .prepareImmediately(true) + .startWhenPrepared(true) + .shouldStream(true) + .getIntent()); + PlaybackPreferences.writeNoMediaPlaying(); + stateManager.stopService(); + return; + } + mediaPlayer.playMediaObject(playable, PlaybackPreferences.getCurrentEpisodeIsStream(), + true, true); + stateManager.validStartCommandWasReceived(); + PlaybackService.this.updateMediaSessionMetadata(playable); + addPlayableToQueue(playable); + }, error -> { + Log.d(TAG, "Playable was not loaded from preferences. Stopping service."); + stateManager.stopService(); + }); } /** @@ -722,9 +756,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { } @Override - public void onSleepTimerAlmostExpired() { - float leftVolume = 0.1f * UserPreferences.getLeftVolume(); - float rightVolume = 0.1f * UserPreferences.getRightVolume(); + public void onSleepTimerAlmostExpired(long timeLeft) { + final float[] multiplicators = {0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f}; + float multiplicator = multiplicators[Math.max(0, (int) timeLeft / 1000)]; + Log.d(TAG, "onSleepTimerAlmostExpired: " + multiplicator); + float leftVolume = multiplicator * UserPreferences.getLeftVolume(); + float rightVolume = multiplicator * UserPreferences.getRightVolume(); mediaPlayer.setVolume(leftVolume, rightVolume); } @@ -965,7 +1002,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { } if (!nextItem.getMedia().localFileAvailable() && !NetworkUtils.isStreamingAllowed() - && UserPreferences.isFollowQueue()) { + && UserPreferences.isFollowQueue() && !nextItem.getFeed().isLocalFeed()) { displayStreamingNotAllowedNotification( new PlaybackServiceStarter(this, nextItem.getMedia()) .prepareImmediately(true) @@ -1154,13 +1191,11 @@ public class PlaybackService extends MediaBrowserServiceCompat { case INITIALIZING: state = PlaybackStateCompat.STATE_CONNECTING; break; - case INITIALIZED: - case INDETERMINATE: - state = PlaybackStateCompat.STATE_NONE; - break; case ERROR: state = PlaybackStateCompat.STATE_ERROR; break; + case INITIALIZED: // Deliberate fall-through + case INDETERMINATE: default: state = PlaybackStateCompat.STATE_NONE; break; @@ -1172,7 +1207,8 @@ public class PlaybackService extends MediaBrowserServiceCompat { long capabilities = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD - | PlaybackStateCompat.ACTION_SKIP_TO_NEXT; + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT + | PlaybackStateCompat.ACTION_SEEK_TO; if (useSkipToPreviousForRewindInLockscreen()) { // Workaround to fool Android so that Lockscreen will expose a skip-to-previous button, @@ -1188,6 +1224,20 @@ public class PlaybackService extends MediaBrowserServiceCompat { capabilities = capabilities | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS; } + UiModeManager uiModeManager = (UiModeManager) getApplicationContext().getSystemService(Context.UI_MODE_SERVICE); + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_CAR) { + sessionState.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( + CUSTOM_ACTION_REWIND, + getString(R.string.rewind_label), R.drawable.ic_notification_fast_rewind) + .build()); + sessionState.addCustomAction( + new PlaybackStateCompat.CustomAction.Builder( + CUSTOM_ACTION_FAST_FORWARD, + getString(R.string.fast_forward_label), R.drawable.ic_notification_fast_forward) + .build()); + } + sessionState.setActions(capabilities); flavorHelper.sessionStateAddActionForWear(sessionState, diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java index 6a892cc1c..632ac07ea 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java @@ -139,6 +139,7 @@ public class PlaybackServiceNotificationBuilder { notification.setSmallIcon(R.drawable.ic_notification); notification.setOngoing(false); notification.setOnlyAlertOnce(true); + notification.setShowWhen(false); notification.setPriority(UserPreferences.getNotifyPriority()); notification.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); notification.setColor(NotificationCompat.COLOR_DEFAULT); @@ -183,15 +184,14 @@ public class PlaybackServiceNotificationBuilder { notification.addAction(R.drawable.ic_notification_pause, //pause action context.getString(R.string.pause_label), pauseButtonPendingIntent); - compactActionList.add(numActions++); } else { PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_PLAY, numActions); notification.addAction(R.drawable.ic_notification_play, //play action context.getString(R.string.play_label), playButtonPendingIntent); - compactActionList.add(numActions++); } + compactActionList.add(numActions++); // ff follows play, then we have skip (if it's present) PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction( diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index afa7fcebf..05d64ea3e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import android.util.Log; import de.danoeh.antennapod.core.preferences.SleepTimerPreferences; +import io.reactivex.disposables.Disposable; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -57,7 +58,7 @@ public class PlaybackServiceTaskManager { private ScheduledFuture<?> widgetUpdaterFuture; private ScheduledFuture<?> sleepTimerFuture; private volatile Future<List<FeedItem>> queueFuture; - private volatile Future<?> chapterLoaderFuture; + private volatile Disposable chapterLoaderFuture; private SleepTimer sleepTimer; @@ -102,7 +103,7 @@ public class PlaybackServiceTaskManager { private synchronized void loadQueue() { if (!isQueueLoaderActive()) { - queueFuture = schedExecutor.submit(DBReader::getQueue); + queueFuture = schedExecutor.submit(() -> DBReader.getQueue()); } } @@ -289,29 +290,20 @@ public class PlaybackServiceTaskManager { } } - private synchronized void cancelChapterLoader() { - if (isChapterLoaderActive()) { - chapterLoaderFuture.cancel(true); - } - } - - private synchronized boolean isChapterLoaderActive() { - return chapterLoaderFuture != null && !chapterLoaderFuture.isDone(); - } - /** * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, * it will be cancelled first. * On completion, the callback's onChapterLoaded method will be called. */ public synchronized void startChapterLoader(@NonNull final Playable media) { - if (isChapterLoaderActive()) { - cancelChapterLoader(); + if (chapterLoaderFuture != null) { + chapterLoaderFuture.dispose(); + chapterLoaderFuture = null; } if (media.getChapters() == null) { - Completable.create(emitter -> { - media.loadChapterMarks(); + chapterLoaderFuture = Completable.create(emitter -> { + media.loadChapterMarks(context); emitter.onComplete(); }) .subscribeOn(Schedulers.io()) @@ -330,7 +322,11 @@ public class PlaybackServiceTaskManager { cancelWidgetUpdater(); disableSleepTimer(); cancelQueueLoader(); - cancelChapterLoader(); + + if (chapterLoaderFuture != null) { + chapterLoaderFuture.dispose(); + chapterLoaderFuture = null; + } } /** @@ -347,7 +343,7 @@ public class PlaybackServiceTaskManager { if (Looper.myLooper() == Looper.getMainLooper()) { // Called in main thread => ExoPlayer is used // Run on ui thread even if called from schedExecutor - Handler handler = new Handler(); + Handler handler = new Handler(Looper.getMainLooper()); return () -> handler.post(runnable); } else { return runnable; @@ -360,7 +356,8 @@ public class PlaybackServiceTaskManager { class SleepTimer implements Runnable { private static final String TAG = "SleepTimer"; private static final long UPDATE_INTERVAL = 1000L; - private static final long NOTIFICATION_THRESHOLD = 10000; + public static final long NOTIFICATION_THRESHOLD = 10000; + private boolean hasVibrated = false; private final long waitingTime; private long timeLeft; private ShakeListener shakeListener; @@ -373,7 +370,7 @@ public class PlaybackServiceTaskManager { if (UserPreferences.useExoplayer() && Looper.myLooper() == Looper.getMainLooper()) { // Run callbacks in main thread so they can call ExoPlayer methods themselves - this.handler = new Handler(); + this.handler = new Handler(Looper.getMainLooper()); } else { this.handler = null; } @@ -390,7 +387,6 @@ public class PlaybackServiceTaskManager { @Override public void run() { Log.d(TAG, "Starting"); - boolean notifiedAlmostExpired = false; long lastTick = System.currentTimeMillis(); while (timeLeft > 0) { try { @@ -405,19 +401,19 @@ public class PlaybackServiceTaskManager { timeLeft -= now - lastTick; lastTick = now; - if (timeLeft < NOTIFICATION_THRESHOLD && !notifiedAlmostExpired) { + if (timeLeft < NOTIFICATION_THRESHOLD) { Log.d(TAG, "Sleep timer is about to expire"); - if (SleepTimerPreferences.vibrate()) { + if (SleepTimerPreferences.vibrate() && !hasVibrated) { Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); if (v != null) { v.vibrate(500); + hasVibrated = true; } } if (shakeListener == null && SleepTimerPreferences.shakeToReset()) { shakeListener = new ShakeListener(context, this); } - postCallback(callback::onSleepTimerAlmostExpired); - notifiedAlmostExpired = true; + postCallback(() -> callback.onSleepTimerAlmostExpired(timeLeft)); } if (timeLeft <= 0) { Log.d(TAG, "Sleep timer expired"); @@ -425,6 +421,7 @@ public class PlaybackServiceTaskManager { shakeListener.pause(); shakeListener = null; } + hasVibrated = false; if (!Thread.currentThread().isInterrupted()) { postCallback(callback::onSleepTimerExpired); } else { @@ -461,7 +458,7 @@ public class PlaybackServiceTaskManager { public interface PSTMCallback { void positionSaverTick(); - void onSleepTimerAlmostExpired(); + void onSleepTimerAlmostExpired(long timeLeft); void onSleepTimerExpired(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java index b0b6e164d..b967577af 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java @@ -58,7 +58,6 @@ class ShakeListener implements SensorEventListener @Override public void onAccuracyChanged(Sensor sensor, int accuracy) { - return; } }
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 7330a6c80..b218a73f9 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -19,6 +19,7 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.LongIntMap; @@ -72,19 +73,13 @@ public final class DBReader { @NonNull private static List<Feed> getFeedList(PodDBAdapter adapter) { - Cursor cursor = null; - try { - cursor = adapter.getAllFeedsCursor(); + try (Cursor cursor = adapter.getAllFeedsCursor()) { List<Feed> feeds = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { Feed feed = extractFeedFromCursorRow(cursor); feeds.add(feed); } return feeds; - } finally { - if (cursor != null) { - cursor.close(); - } } } @@ -96,18 +91,13 @@ public final class DBReader { public static List<String> getFeedListDownloadUrls() { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getFeedCursorDownloadUrls(); + try (Cursor cursor = adapter.getFeedCursorDownloadUrls()) { List<String> result = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { result.add(cursor.getString(1)); } return result; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -173,9 +163,7 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getAllItemsOfFeedCursor(feed); + try (Cursor cursor = adapter.getAllItemsOfFeedCursor(feed)) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); Collections.sort(items, new FeedItemPubdateComparator()); for (FeedItem item : items) { @@ -183,9 +171,6 @@ public final class DBReader { } return items; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -227,16 +212,10 @@ public final class DBReader { @NonNull static List<FeedItem> getQueue(PodDBAdapter adapter) { Log.d(TAG, "getQueue()"); - Cursor cursor = null; - try { - cursor = adapter.getQueueCursor(); + try (Cursor cursor = adapter.getQueueCursor()) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); return items; - } finally { - if (cursor != null) { - cursor.close(); - } } } @@ -258,18 +237,12 @@ public final class DBReader { } private static LongList getQueueIDList(PodDBAdapter adapter) { - Cursor cursor = null; - try { - cursor = adapter.getQueueIDCursor(); + try (Cursor cursor = adapter.getQueueIDCursor()) { LongList queueIds = new LongList(cursor.getCount()); while (cursor.moveToNext()) { queueIds.add(cursor.getLong(0)); } return queueIds; - } finally { - if (cursor != null) { - cursor.close(); - } } } @@ -303,17 +276,12 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getDownloadedItemsCursor(); + try (Cursor cursor = adapter.getDownloadedItemsCursor()) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); Collections.sort(items, new FeedItemPubdateComparator()); return items; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -329,16 +297,11 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getPlayedItemsCursor(); + try (Cursor cursor = adapter.getPlayedItemsCursor()) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); return items; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -356,16 +319,11 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getNewItemsCursor(offset, limit); + try (Cursor cursor = adapter.getNewItemsCursor(offset, limit)) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); return items; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -382,16 +340,11 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getFavoritesCursor(offset, limit); + try (Cursor cursor = adapter.getFavoritesCursor(offset, limit)) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); return items; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -401,18 +354,13 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getFavoritesCursor(0, Integer.MAX_VALUE); + try (Cursor cursor = adapter.getFavoritesCursor(0, Integer.MAX_VALUE)) { LongList favoriteIDs = new LongList(cursor.getCount()); while (cursor.moveToNext()) { favoriteIDs.add(cursor.getLong(0)); } return favoriteIDs; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -429,16 +377,11 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit); + try (Cursor cursor = adapter.getRecentlyPublishedItemsCursor(offset, limit)) { List<FeedItem> items = extractItemlistFromCursor(adapter, cursor); loadAdditionalFeedItemListData(items); return items; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -493,9 +436,7 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE); + try (Cursor cursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE)) { List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { downloadLog.add(DownloadStatus.fromCursor(cursor)); @@ -503,9 +444,6 @@ public final class DBReader { Collections.sort(downloadLog, new DownloadStatusComparator()); return downloadLog; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -522,9 +460,7 @@ public final class DBReader { PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId); + try (Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feedId)) { List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount()); while (cursor.moveToNext()) { downloadLog.add(DownloadStatus.fromCursor(cursor)); @@ -532,9 +468,6 @@ public final class DBReader { Collections.sort(downloadLog, new DownloadStatusComparator()); return downloadLog; } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -561,9 +494,7 @@ public final class DBReader { @Nullable static Feed getFeed(final long feedId, PodDBAdapter adapter) { Feed feed = null; - Cursor cursor = null; - try { - cursor = adapter.getFeedCursor(feedId); + try (Cursor cursor = adapter.getFeedCursor(feedId)) { if (cursor.moveToNext()) { feed = extractFeedFromCursorRow(cursor); feed.setItems(getFeedItemList(feed)); @@ -571,10 +502,6 @@ public final class DBReader { Log.e(TAG, "getFeed could not find feed with id " + feedId); } return feed; - } finally { - if (cursor != null) { - cursor.close(); - } } } @@ -583,9 +510,7 @@ public final class DBReader { Log.d(TAG, "Loading feeditem with id " + itemId); FeedItem item = null; - Cursor cursor = null; - try { - cursor = adapter.getFeedItemCursor(Long.toString(itemId)); + try (Cursor cursor = adapter.getFeedItemCursor(Long.toString(itemId))) { if (cursor.moveToNext()) { List<FeedItem> list = extractItemlistFromCursor(adapter, cursor); if (!list.isEmpty()) { @@ -594,10 +519,6 @@ public final class DBReader { } } return item; - } finally { - if (cursor != null) { - cursor.close(); - } } } @@ -632,9 +553,7 @@ public final class DBReader { @Nullable private static FeedItem getFeedItemByUrl(final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) { Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); - Cursor cursor = null; - try { - cursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl); + try (Cursor cursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl)) { if (!cursor.moveToNext()) { return null; } @@ -643,10 +562,6 @@ public final class DBReader { return list.get(0); } return null; - } finally { - if (cursor != null) { - cursor.close(); - } } } @@ -669,10 +584,8 @@ public final class DBReader { } private static String getImageAuthentication(final String imageUrl, PodDBAdapter adapter) { - String credentials = null; - Cursor cursor = null; - try { - cursor = adapter.getImageAuthenticationCursor(imageUrl); + String credentials; + try (Cursor cursor = adapter.getImageAuthenticationCursor(imageUrl)) { if (cursor.moveToFirst()) { String username = cursor.getString(0); String password = cursor.getString(1); @@ -684,10 +597,6 @@ public final class DBReader { } else { credentials = ""; } - } finally { - if (cursor != null) { - cursor.close(); - } } return credentials; } @@ -721,9 +630,7 @@ public final class DBReader { Log.d(TAG, "loadDescriptionOfFeedItem() called with: " + "item = [" + item + "]"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = null; - try { - cursor = adapter.getDescriptionOfItem(item); + try (Cursor cursor = adapter.getDescriptionOfItem(item)) { if (cursor.moveToFirst()) { int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION); String description = cursor.getString(indexDescription); @@ -733,9 +640,6 @@ public final class DBReader { item.setContentEncoded(contentEncoded); } } finally { - if (cursor != null) { - cursor.close(); - } adapter.close(); } } @@ -747,29 +651,30 @@ public final class DBReader { * * @param item The FeedItem */ - public static void loadChaptersOfFeedItem(final FeedItem item) { + public static List<Chapter> loadChaptersOfFeedItem(final FeedItem item) { Log.d(TAG, "loadChaptersOfFeedItem() called with: " + "item = [" + item + "]"); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); try { - loadChaptersOfFeedItem(adapter, item); + return loadChaptersOfFeedItem(adapter, item); } finally { adapter.close(); } } - private static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) { + private static List<Chapter> loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) { try (Cursor cursor = adapter.getSimpleChaptersOfFeedItemCursor(item)) { int chaptersCount = cursor.getCount(); if (chaptersCount == 0) { item.setChapters(null); - return; + return null; } - item.setChapters(new ArrayList<>(chaptersCount)); + ArrayList<Chapter> chapters = new ArrayList<>(); while (cursor.moveToNext()) { - item.getChapters().add(Chapter.fromCursor(cursor)); + chapters.add(Chapter.fromCursor(cursor)); } + return chapters; } } @@ -842,6 +747,7 @@ public final class DBReader { long episodesStarted = 0; long episodesStartedIncludingMarked = 0; long totalDownloadSize = 0; + long episodesDownloadCount = 0; List<FeedItem> items = getFeed(feed.getId()).getItems(); for (FeedItem item : items) { FeedMedia media = item.getMedia(); @@ -869,13 +775,14 @@ public final class DBReader { if (media.isDownloaded()) { totalDownloadSize = totalDownloadSize + media.getSize(); + episodesDownloadCount++; } episodes++; } feedTime.add(new StatisticsItem( feed, feedTotalTime, feedPlayedTime, feedPlayedTimeCountAll, episodes, - episodesStarted, episodesStartedIncludingMarked, totalDownloadSize)); + episodesStarted, episodesStartedIncludingMarked, totalDownloadSize, episodesDownloadCount)); } adapter.close(); @@ -892,6 +799,7 @@ public final class DBReader { Log.d(TAG, "getNavDrawerData() called with: " + ""); PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); + List<Feed> feeds = getFeedList(adapter); long[] feedIds = new long[feeds.size()]; for (int i = 0; i < feeds.size(); i++) { @@ -899,6 +807,9 @@ public final class DBReader { } final LongIntMap feedCounters = adapter.getFeedCounters(feedIds); + SubscriptionsFilter subscriptionsFilter = UserPreferences.getSubscriptionsFilter(); + feeds = subscriptionsFilter.filter(getFeedList(adapter), feedCounters); + Comparator<Feed> comparator; int feedOrder = UserPreferences.getFeedOrder(); if (feedOrder == UserPreferences.FEED_ORDER_COUNTER) { @@ -942,24 +853,11 @@ public final class DBReader { } }; } else { + final Map<Long, Long> recentPubDates = adapter.getMostRecentItemDates(); comparator = (lhs, rhs) -> { - if (lhs.getItems() == null || lhs.getItems().size() == 0) { - List<FeedItem> items = DBReader.getFeedItemList(lhs); - lhs.setItems(items); - } - if (rhs.getItems() == null || rhs.getItems().size() == 0) { - List<FeedItem> items = DBReader.getFeedItemList(rhs); - rhs.setItems(items); - } - if (lhs.getMostRecentItem() == null) { - return 1; - } else if (rhs.getMostRecentItem() == null) { - return -1; - } else { - Date d1 = lhs.getMostRecentItem().getPubDate(); - Date d2 = rhs.getMostRecentItem().getPubDate(); - return d2.compareTo(d1); - } + long dateLhs = recentPubDates.containsKey(lhs.getId()) ? recentPubDates.get(lhs.getId()) : 0; + long dateRhs = recentPubDates.containsKey(rhs.getId()) ? recentPubDates.get(rhs.getId()) : 0; + return Long.compare(dateRhs, dateLhs); }; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index 16e2825b4..c059e696a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -16,6 +16,7 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.feed.LocalFeedUpdater; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.sync.SyncService; @@ -241,7 +242,12 @@ public final class DBTasks { feed.getPreferences().getUsername(), feed.getPreferences().getPassword()); } f.setId(feed.getId()); - DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser); + + if (f.isLocalFeed()) { + new Thread(() -> LocalFeedUpdater.updateFeed(f, context)).start(); + } else { + DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages, force, initiatedByUser); + } } /** @@ -257,7 +263,6 @@ public final class DBTasks { EventBus.getDefault().post(new MessageEvent(context.getString(R.string.error_file_not_found))); } - @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public static List<? extends FeedItem> enqueueFeedItemsToDownload(final Context context, List<? extends FeedItem> items) throws InterruptedException, ExecutionException { List<FeedItem> itemsToEnqueue = new ArrayList<>(); @@ -367,118 +372,135 @@ public final class DBTasks { * <p/> * This method should NOT be executed on the GUI thread. * - * @param context Used for accessing the DB. - * @param newFeeds The new Feed objects. - * @return The updated Feeds from the database if it already existed, or the new Feed from the parameters otherwise. + * @param context Used for accessing the DB. + * @param newFeed The new Feed object. + * @param removeUnlistedItems The item list in the new Feed object is considered to be exhaustive. + * I.e. items are removed from the database if they are not in this item list. + * @return The updated Feed from the database if it already existed, or the new Feed from the parameters otherwise. */ - public static synchronized Feed[] updateFeed(final Context context, - final Feed... newFeeds) { - List<Feed> newFeedsList = new ArrayList<>(); - List<Feed> updatedFeedsList = new ArrayList<>(); - Feed[] resultFeeds = new Feed[newFeeds.length]; + public static synchronized Feed updateFeed(Context context, Feed newFeed, boolean removeUnlistedItems) { + Feed resultFeed; + List<FeedItem> unlistedItems = new ArrayList<>(); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) { + // Look up feed in the feedslist + final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed); + if (savedFeed == null) { + Log.d(TAG, "Found no existing Feed with title " + + newFeed.getTitle() + ". Adding as new one."); + + // Add a new Feed + // all new feeds will have the most recent item marked as unplayed + FeedItem mostRecent = newFeed.getMostRecentItem(); + if (mostRecent != null) { + mostRecent.setNew(); + } - final Feed newFeed = newFeeds[feedIdx]; + resultFeed = newFeed; + } else { + Log.d(TAG, "Feed with title " + newFeed.getTitle() + + " already exists. Syncing new with existing one."); - // Look up feed in the feedslist - final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter, - newFeed); - if (savedFeed == null) { - Log.d(TAG, "Found no existing Feed with title " - + newFeed.getTitle() + ". Adding as new one."); - - // Add a new Feed - // all new feeds will have the most recent item marked as unplayed - FeedItem mostRecent = newFeed.getMostRecentItem(); - if (mostRecent != null) { - mostRecent.setNew(); - } + Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); - newFeedsList.add(newFeed); - resultFeeds[feedIdx] = newFeed; + if (newFeed.getPageNr() == savedFeed.getPageNr()) { + if (savedFeed.compareWithOther(newFeed)) { + Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes"); + savedFeed.updateFromOther(newFeed); + } } else { - Log.d(TAG, "Feed with title " + newFeed.getTitle() - + " already exists. Syncing new with existing one."); + Log.d(TAG, "New feed has a higher page number."); + savedFeed.setNextPageLink(newFeed.getNextPageLink()); + } + if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) { + Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences"); + savedFeed.getPreferences().updateFromOther(newFeed.getPreferences()); + } - Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); + // get the most recent date now, before we start changing the list + FeedItem priorMostRecent = savedFeed.getMostRecentItem(); + Date priorMostRecentDate = null; + if (priorMostRecent != null) { + priorMostRecentDate = priorMostRecent.getPubDate(); + } - if (newFeed.getPageNr() == savedFeed.getPageNr()) { - if (savedFeed.compareWithOther(newFeed)) { - Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes"); - savedFeed.updateFromOther(newFeed); + // Look for new or updated Items + for (int idx = 0; idx < newFeed.getItems().size(); idx++) { + final FeedItem item = newFeed.getItems().get(idx); + FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, item.getIdentifyingValue()); + if (oldItem == null) { + // item is new + item.setFeed(savedFeed); + item.setAutoDownload(savedFeed.getPreferences().getAutoDownload()); + + if (idx >= savedFeed.getItems().size()) { + savedFeed.getItems().add(item); + } else { + savedFeed.getItems().add(idx, item); } - } else { - Log.d(TAG, "New feed has a higher page number."); - savedFeed.setNextPageLink(newFeed.getNextPageLink()); - } - if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) { - Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences"); - savedFeed.getPreferences().updateFromOther(newFeed.getPreferences()); - } - // get the most recent date now, before we start changing the list - FeedItem priorMostRecent = savedFeed.getMostRecentItem(); - Date priorMostRecentDate = null; - if (priorMostRecent != null) { - priorMostRecentDate = priorMostRecent.getPubDate(); + // only mark the item new if it was published after or at the same time + // as the most recent item + // (if the most recent date is null then we can assume there are no items + // and this is the first, hence 'new') + if (priorMostRecentDate == null + || priorMostRecentDate.before(item.getPubDate()) + || priorMostRecentDate.equals(item.getPubDate())) { + Log.d(TAG, "Marking item published on " + item.getPubDate() + + " new, prior most recent date = " + priorMostRecentDate); + item.setNew(); + } + } else { + oldItem.updateFromOther(item); } + } - // Look for new or updated Items - for (int idx = 0; idx < newFeed.getItems().size(); idx++) { - final FeedItem item = newFeed.getItems().get(idx); - FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, - item.getIdentifyingValue()); - if (oldItem == null) { - // item is new - item.setFeed(savedFeed); - item.setAutoDownload(savedFeed.getPreferences().getAutoDownload()); - - if (idx >= savedFeed.getItems().size()) { - savedFeed.getItems().add(item); - } else { - savedFeed.getItems().add(idx, item); - } - - // only mark the item new if it was published after or at the same time - // as the most recent item - // (if the most recent date is null then we can assume there are no items - // and this is the first, hence 'new') - if (priorMostRecentDate == null - || priorMostRecentDate.before(item.getPubDate()) - || priorMostRecentDate.equals(item.getPubDate())) { - Log.d(TAG, "Marking item published on " + item.getPubDate() - + " new, prior most recent date = " + priorMostRecentDate); - item.setNew(); - } - } else { - oldItem.updateFromOther(item); + // identify items to be removed + if (removeUnlistedItems) { + Iterator<FeedItem> it = savedFeed.getItems().iterator(); + while (it.hasNext()) { + FeedItem feedItem = it.next(); + if (searchFeedItemByIdentifyingValue(newFeed, feedItem.getIdentifyingValue()) == null) { + unlistedItems.add(feedItem); + it.remove(); } } - // update attributes - savedFeed.setLastUpdate(newFeed.getLastUpdate()); - savedFeed.setType(newFeed.getType()); - savedFeed.setLastUpdateFailed(false); - - updatedFeedsList.add(savedFeed); - resultFeeds[feedIdx] = savedFeed; } - } - adapter.close(); + // update attributes + savedFeed.setLastUpdate(newFeed.getLastUpdate()); + savedFeed.setType(newFeed.getType()); + savedFeed.setLastUpdateFailed(false); + + resultFeed = savedFeed; + } try { - DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[0])).get(); - DBWriter.setCompleteFeed(updatedFeedsList.toArray(new Feed[0])).get(); + if (savedFeed == null) { + DBWriter.addNewFeed(context, newFeed).get(); + // Update with default values that are set in database + resultFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed); + } else { + DBWriter.setCompleteFeed(savedFeed).get(); + } + if (removeUnlistedItems) { + DBWriter.deleteFeedItems(context, unlistedItems).get(); + } } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } - EventBus.getDefault().post(new FeedListUpdateEvent(updatedFeedsList)); + adapter.close(); + + if (savedFeed != null) { + EventBus.getDefault().post(new FeedListUpdateEvent(savedFeed)); + } else { + EventBus.getDefault().post(new FeedListUpdateEvent(Collections.emptyList())); + } - return resultFeeds; + return resultFeed; } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index e33b67719..9e6041df3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -141,48 +141,74 @@ public class DBWriter { return dbExec.submit(() -> { DownloadRequester requester = DownloadRequester.getInstance(); final Feed feed = DBReader.getFeed(feedId); + if (feed == null) { + return; + } - if (feed != null) { - // delete stored media files and mark them as read - List<FeedItem> queue = DBReader.getQueue(); - List<FeedItem> removed = new ArrayList<>(); - if (feed.getItems() == null) { - DBReader.getFeedItemList(feed); - } + // delete stored media files and mark them as read + if (feed.getItems() == null) { + DBReader.getFeedItemList(feed); + } + deleteFeedItemsSynchronous(context, feed.getItems()); - for (FeedItem item : feed.getItems()) { - if (queue.remove(item)) { - removed.add(item); - } - if (item.getMedia() != null && item.getMedia().isDownloaded()) { - deleteFeedMediaSynchronous(context, item.getMedia()); - } else if (item.getMedia() != null && requester.isDownloadingFile(item.getMedia())) { - requester.cancelDownload(context, item.getMedia()); - } - } - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - if (removed.size() > 0) { - adapter.setQueue(queue); - for (FeedItem item : removed) { - EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item)); - } - } - adapter.removeFeed(feed); - adapter.close(); + // delete feed + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.removeFeed(feed); + adapter.close(); - SyncService.enqueueFeedRemoved(context, feed.getDownload_url()); - EventBus.getDefault().post(new FeedListUpdateEvent(feed)); + SyncService.enqueueFeedRemoved(context, feed.getDownload_url()); + EventBus.getDefault().post(new FeedListUpdateEvent(feed)); + }); + } - // we assume we also removed download log entries for the feed or its media files. - // especially important if download or refresh failed, as the user should not be able - // to retry these - EventBus.getDefault().post(DownloadLogEvent.listUpdated()); + /** + * Remove the listed items and their FeedMedia entries. + * Deleting media also removes the download log entries. + */ + @NonNull + public static Future<?> deleteFeedItems(@NonNull Context context, @NonNull List<FeedItem> items) { + return dbExec.submit(() -> deleteFeedItemsSynchronous(context, items)); + } - BackupManager backupManager = new BackupManager(context); - backupManager.dataChanged(); + /** + * Remove the listed items and their FeedMedia entries. + * Deleting media also removes the download log entries. + */ + private static void deleteFeedItemsSynchronous(@NonNull Context context, @NonNull List<FeedItem> items) { + DownloadRequester requester = DownloadRequester.getInstance(); + List<FeedItem> queue = DBReader.getQueue(); + List<FeedItem> removedFromQueue = new ArrayList<>(); + for (FeedItem item : items) { + if (queue.remove(item)) { + removedFromQueue.add(item); } - }); + if (item.getMedia() != null && item.getMedia().isDownloaded()) { + deleteFeedMediaSynchronous(context, item.getMedia()); + } else if (item.getMedia() != null && requester.isDownloadingFile(item.getMedia())) { + requester.cancelDownload(context, item.getMedia()); + } + } + + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + if (!removedFromQueue.isEmpty()) { + adapter.setQueue(queue); + } + adapter.removeFeedItems(items); + adapter.close(); + + for (FeedItem item : removedFromQueue) { + EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item)); + } + + // we assume we also removed download log entries for the feed or its media files. + // especially important if download or refresh failed, as the user should not be able + // to retry these + EventBus.getDefault().post(DownloadLogEvent.listUpdated()); + + BackupManager backupManager = new BackupManager(context); + backupManager.dataChanged(); } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java index 234c01b20..271babc6e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DatabaseExporter.java @@ -5,6 +5,7 @@ import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import android.net.Uri; import android.os.ParcelFileDescriptor; +import android.text.format.Formatter; import android.util.Log; import de.danoeh.antennapod.core.R; import org.apache.commons.io.FileUtils; @@ -53,7 +54,16 @@ public class DatabaseExporter { if (currentDB.exists()) { src = new FileInputStream(currentDB).getChannel(); dst = outFileStream.getChannel(); - dst.transferFrom(src, 0, src.size()); + long srcSize = src.size(); + dst.transferFrom(src, 0, srcSize); + + long newDstSize = dst.size(); + if (newDstSize != srcSize) { + throw new IOException(String.format( + "Unable to write entire database. Expected to write %s, but wrote %s.", + Formatter.formatShortFileSize(context, srcSize), + Formatter.formatShortFileSize(context, newDstSize))); + } } else { throw new IOException("Can not access current database"); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java index f10dde65f..e3121caa2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -340,7 +340,7 @@ public class DownloadRequester implements DownloadStateProvider { /** * Checks if feedfile is in the downloads list */ - public synchronized boolean isDownloadingFile(FeedFile item) { + public synchronized boolean isDownloadingFile(@NonNull FeedFile item) { return item.getDownload_url() != null && downloads.containsKey(item.getDownload_url()); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index e6d47b32a..935b06cd6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -14,15 +14,18 @@ import android.database.sqlite.SQLiteOpenHelper; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; -import java.util.Collections; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import de.danoeh.antennapod.core.feed.Chapter; @@ -357,6 +360,21 @@ public class PodDBAdapter { // do nothing } + /** + * <p>Resets all database connections to ensure new database connections for + * the next test case. Call method only for unit tests.</p> + * + * <p>That's a workaround for a Robolectric issue in ShadowSQLiteConnection + * that leads to an error <tt>IllegalStateException: Illegal connection + * pointer</tt> if several threads try to use the same database connection. + * For more information see + * <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p> + */ + public static void tearDownTests() { + db = null; + SingletonHolder.dbHelper.close(); + } + public static boolean deleteDatabase() { PodDBAdapter adapter = getInstance(); adapter.open(); @@ -593,6 +611,11 @@ public class PodDBAdapter { * @return the id of the entry */ private long setFeedItem(FeedItem item, boolean saveFeed) { + if (item.getId() == 0 && item.getPubDate() == null) { + Log.e(TAG, "Newly saved item has no pubDate. Using current date as pubDate"); + item.setPubDate(new Date()); + } + ContentValues values = new ContentValues(); values.put(KEY_TITLE, item.getTitle()); values.put(KEY_LINK, item.getLink()); @@ -852,6 +875,23 @@ public class PodDBAdapter { } /** + * Remove the listed items and their FeedMedia entries. + */ + public void removeFeedItems(@NonNull List<FeedItem> items) { + try { + db.beginTransactionNonExclusive(); + for (FeedItem item : items) { + removeFeedItem(item); + } + db.setTransactionSuccessful(); + } catch (SQLException e) { + Log.e(TAG, Log.getStackTraceString(e)); + } finally { + db.endTransaction(); + } + } + + /** * Remove a feed with all its FeedItems and Media entries. */ public void removeFeed(Feed feed) { @@ -1184,6 +1224,25 @@ public class PodDBAdapter { return conditionalFeedCounterRead(whereRead, feedIds); } + public final Map<Long, Long> getMostRecentItemDates() { + final String query = "SELECT " + KEY_FEED + "," + + " MAX(" + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE + ") AS most_recent_pubdate" + + " FROM " + TABLE_NAME_FEED_ITEMS + + " GROUP BY " + KEY_FEED; + + Cursor c = db.rawQuery(query, null); + Map<Long, Long> result = new HashMap<>(); + if (c.moveToFirst()) { + do { + long feedId = c.getLong(0); + long date = c.getLong(1); + result.put(feedId, date); + } while (c.moveToNext()); + } + c.close(); + return result; + } + public final int getNumberOfDownloadedEpisodes() { final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_DOWNLOADED + " > 0"; @@ -1201,12 +1260,17 @@ public class PodDBAdapter { * Uses DatabaseUtils to escape a search query and removes ' at the * beginning and the end of the string returned by the escape method. */ - private String prepareSearchQuery(String query) { - StringBuilder builder = new StringBuilder(); - DatabaseUtils.appendEscapedSQLString(builder, query); - builder.deleteCharAt(0); - builder.deleteCharAt(builder.length() - 1); - return builder.toString(); + private String[] prepareSearchQuery(String query) { + String[] queryWords = query.split("\\s+"); + for (int i = 0; i < queryWords.length; ++i) { + StringBuilder builder = new StringBuilder(); + DatabaseUtils.appendEscapedSQLString(builder, queryWords[i]); + builder.deleteCharAt(0); + builder.deleteCharAt(builder.length() - 1); + queryWords[i] = builder.toString(); + } + + return queryWords; } /** @@ -1216,9 +1280,9 @@ public class PodDBAdapter { * @return A cursor with all search results in SEL_FI_EXTRA selection. */ public Cursor searchItems(long feedID, String searchQuery) { - String preparedQuery = prepareSearchQuery(searchQuery); + String[] queryWords = prepareSearchQuery(searchQuery); - String queryFeedId = ""; + String queryFeedId; if (feedID != 0) { // search items in specific feed queryFeedId = KEY_FEED + " = " + feedID; @@ -1227,14 +1291,28 @@ public class PodDBAdapter { queryFeedId = "1 = 1"; } - String query = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION - + " WHERE " + queryFeedId + " AND (" - + KEY_DESCRIPTION + " LIKE '%" + preparedQuery + "%' OR " - + KEY_CONTENT_ENCODED + " LIKE '%" + preparedQuery + "%' OR " - + KEY_TITLE + " LIKE '%" + preparedQuery + "%'" - + ") ORDER BY " + KEY_PUBDATE + " DESC " - + "LIMIT 300"; - return db.rawQuery(query, null); + String queryStart = SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION + + " WHERE " + queryFeedId + " AND ("; + StringBuilder sb = new StringBuilder(queryStart); + + for (int i = 0; i < queryWords.length; i++) { + sb + .append("(") + .append(KEY_DESCRIPTION + " LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_CONTENT_ENCODED).append(" LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i]) + .append("%') "); + + if (i != queryWords.length - 1) { + sb.append("AND "); + } + } + + sb.append(") ORDER BY " + KEY_PUBDATE + " DESC LIMIT 300"); + + return db.rawQuery(sb.toString(), null); } /** @@ -1243,15 +1321,31 @@ public class PodDBAdapter { * @return A cursor with all search results in SEL_FI_EXTRA selection. */ public Cursor searchFeeds(String searchQuery) { - String preparedQuery = prepareSearchQuery(searchQuery); - String query = "SELECT * FROM " + TABLE_NAME_FEEDS + " WHERE " - + KEY_TITLE + " LIKE '%" + preparedQuery + "%' OR " - + KEY_CUSTOM_TITLE + " LIKE '%" + preparedQuery + "%' OR " - + KEY_AUTHOR + " LIKE '%" + preparedQuery + "%' OR " - + KEY_DESCRIPTION + " LIKE '%" + preparedQuery + "%' " - + "ORDER BY " + KEY_TITLE + " ASC " - + "LIMIT 300"; - return db.rawQuery(query, null); + String[] queryWords = prepareSearchQuery(searchQuery); + + String queryStart = "SELECT * FROM " + TABLE_NAME_FEEDS + " WHERE "; + StringBuilder sb = new StringBuilder(queryStart); + + for (int i = 0; i < queryWords.length; i++) { + sb + .append("(") + .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_CUSTOM_TITLE).append(" LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_AUTHOR).append(" LIKE '%").append(queryWords[i]) + .append("%' OR ") + .append(KEY_DESCRIPTION).append(" LIKE '%").append(queryWords[i]) + .append("%') "); + + if (i != queryWords.length - 1) { + sb.append("AND "); + } + } + + sb.append("ORDER BY " + KEY_TITLE + " ASC LIMIT 300"); + + return db.rawQuery(sb.toString(), null); } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java b/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java index f96af185b..18a5403a7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/StatisticsItem.java @@ -36,9 +36,14 @@ public class StatisticsItem { */ public final long totalDownloadSize; + /** + * Stores the number of episodes downloaded. + */ + public final long episodesDownloadCount; + public StatisticsItem(Feed feed, long time, long timePlayed, long timePlayedCountAll, long episodes, long episodesStarted, long episodesStartedIncludingMarked, - long totalDownloadSize) { + long totalDownloadSize, long episodesDownloadCount) { this.feed = feed; this.time = time; this.timePlayed = timePlayed; @@ -47,5 +52,6 @@ public class StatisticsItem { this.episodesStarted = episodesStarted; this.episodesStartedIncludingMarked = episodesStartedIncludingMarked; this.totalDownloadSize = totalDownloadSize; + this.episodesDownloadCount = episodesDownloadCount; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java index 4c89ebc19..1f5d9b75f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java @@ -39,6 +39,7 @@ import de.danoeh.antennapod.core.sync.model.ISyncService; import de.danoeh.antennapod.core.sync.model.SubscriptionChanges; import de.danoeh.antennapod.core.sync.model.SyncServiceException; import de.danoeh.antennapod.core.sync.model.UploadChangesResponse; +import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.URLChecker; import de.danoeh.antennapod.core.util.gui.NotificationUtils; import io.reactivex.Completable; @@ -456,7 +457,7 @@ public class SyncService extends Worker { break; } } - + LongList queueToBeRemoved = new LongList(); List<FeedItem> updatedItems = new ArrayList<>(); for (EpisodeAction action : mostRecentPlayAction.values()) { FeedItem playItem = DBReader.getFeedItemByUrl(action.getPodcast(), action.getEpisode()); @@ -467,10 +468,12 @@ public class SyncService extends Worker { if (playItem.getMedia().hasAlmostEnded()) { Log.d(TAG, "Marking as played"); playItem.setPlayed(true); + queueToBeRemoved.add(playItem.getId()); } updatedItems.add(playItem); } } + DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray()); DBWriter.setItemList(updatedItems); } @@ -491,7 +494,7 @@ public class SyncService extends Worker { PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT); Notification notification = new NotificationCompat.Builder(getApplicationContext(), - NotificationUtils.CHANNEL_ID_ERROR) + NotificationUtils.CHANNEL_ID_SYNC_ERROR) .setContentTitle(getApplicationContext().getString(R.string.gpodnetsync_error_title)) .setContentText(description) .setContentIntent(pendingIntent) diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java index eae7a08af..62c8ce5f3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java @@ -21,7 +21,6 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; -import org.apache.commons.io.Charsets; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -34,6 +33,7 @@ import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedList; @@ -505,7 +505,7 @@ public class GpodnetService implements ISyncService { RequestBody requestBody = RequestBody.create(TEXT, ""); Request request = new Request.Builder().url(url).post(requestBody).build(); try { - String credential = Credentials.basic(username, password, Charsets.UTF_8); + String credential = Credentials.basic(username, password, Charset.forName("UTF-8")); Request authRequest = request.newBuilder().header("Authorization", credential).build(); Response response = httpClient.newCall(authRequest).execute(); checkStatusCode(response); @@ -519,8 +519,8 @@ public class GpodnetService implements ISyncService { private String executeRequest(@NonNull Request.Builder requestB) throws GpodnetServiceException { Request request = requestB.build(); - String responseString = null; - Response response = null; + String responseString; + Response response; ResponseBody body = null; try { diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java b/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java index 6154ccc84..798be8d96 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeAction.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.sync.model; import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import androidx.core.util.ObjectsCompat; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.util.DateUtils; @@ -179,6 +180,7 @@ public class EpisodeAction { return obj; } + @NonNull @Override public String toString() { return "EpisodeAction{" diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java b/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java index 77942ffa0..90af585af 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/model/EpisodeActionChanges.java @@ -23,6 +23,7 @@ public class EpisodeActionChanges { return this.timestamp; } + @NonNull @Override public String toString() { return "EpisodeActionGetResponse{" diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java index e85d5fae1..0c0561279 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java @@ -1,7 +1,7 @@ package de.danoeh.antennapod.core.syndication.namespace.atom; -import android.os.Build; -import android.text.Html; +import androidx.core.text.HtmlCompat; + import de.danoeh.antennapod.core.syndication.namespace.Namespace; import de.danoeh.antennapod.core.syndication.namespace.SyndElement; @@ -24,11 +24,7 @@ public class AtomText extends SyndElement { if (type == null) { return content; } else if (type.equals(TYPE_HTML)) { - if (Build.VERSION.SDK_INT >= 24) { - return Html.fromHtml(content, Html.FROM_HTML_MODE_LEGACY).toString(); - } else { - return Html.fromHtml(content).toString(); - } + return HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_LEGACY).toString(); } else if (type.equals(TYPE_XHTML)) { return content; } else { // Handle as text by default diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java index 737f902b7..d4a2cdca6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java @@ -1,12 +1,12 @@ package de.danoeh.antennapod.core.util; +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.Log; import java.net.URLConnection; -import java.util.zip.CheckedOutputStream; - import de.danoeh.antennapod.core.ClientConfig; import org.apache.commons.io.IOUtils; @@ -52,65 +52,73 @@ public class ChapterUtils { return chapters.size() - 1; } - public static void loadChaptersFromStreamUrl(Playable media) { - ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media); - if (media.getChapters() == null) { - ChapterUtils.readOggChaptersFromPlayableStreamUrl(media); + public static List<Chapter> loadChaptersFromStreamUrl(Playable media, Context context) { + List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media, context); + if (chapters == null) { + chapters = ChapterUtils.readOggChaptersFromPlayableStreamUrl(media, context); } + return chapters; } - public static void loadChaptersFromFileUrl(Playable media) { + public static List<Chapter> loadChaptersFromFileUrl(Playable media) { if (!media.localFileAvailable()) { Log.e(TAG, "Could not load chapters from file url: local file not available"); - return; + return null; } - ChapterUtils.readID3ChaptersFromPlayableFileUrl(media); - if (media.getChapters() == null) { - ChapterUtils.readOggChaptersFromPlayableFileUrl(media); + List<Chapter> chapters = ChapterUtils.readID3ChaptersFromPlayableFileUrl(media); + if (chapters == null) { + chapters = ChapterUtils.readOggChaptersFromPlayableFileUrl(media); } + return chapters; } /** * Uses the download URL of a media object of a feeditem to read its ID3 * chapters. */ - private static void readID3ChaptersFromPlayableStreamUrl(Playable p) { + private static List<Chapter> readID3ChaptersFromPlayableStreamUrl(Playable p, Context context) { if (p == null || p.getStreamUrl() == null) { Log.e(TAG, "Unable to read ID3 chapters: media or download URL was null"); - return; + return null; } Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); CountingInputStream in = null; try { - URL url = new URL(p.getStreamUrl()); - URLConnection urlConnection = url.openConnection(); - urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT); - in = new CountingInputStream(urlConnection.getInputStream()); + if (p.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) { + Uri uri = Uri.parse(p.getStreamUrl()); + in = new CountingInputStream(context.getContentResolver().openInputStream(uri)); + } else { + URL url = new URL(p.getStreamUrl()); + URLConnection urlConnection = url.openConnection(); + urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT); + in = new CountingInputStream(urlConnection.getInputStream()); + } List<Chapter> chapters = readChaptersFrom(in); if (!chapters.isEmpty()) { - p.setChapters(chapters); + return chapters; } Log.i(TAG, "Chapters loaded"); - } catch (IOException | ID3ReaderException e) { + } catch (IOException | ID3ReaderException | IllegalArgumentException e) { Log.e(TAG, Log.getStackTraceString(e)); } finally { IOUtils.closeQuietly(in); } + return null; } /** * Uses the file URL of a media object of a feeditem to read its ID3 * chapters. */ - private static void readID3ChaptersFromPlayableFileUrl(Playable p) { + private static List<Chapter> readID3ChaptersFromPlayableFileUrl(Playable p) { if (p == null || !p.localFileAvailable() || p.getLocalMediaUrl() == null) { - return; + return null; } Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); File source = new File(p.getLocalMediaUrl()); if (!source.exists()) { Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist"); - return; + return null; } CountingInputStream in = null; @@ -118,7 +126,7 @@ public class ChapterUtils { in = new CountingInputStream(new BufferedInputStream(new FileInputStream(source))); List<Chapter> chapters = readChaptersFrom(in); if (!chapters.isEmpty()) { - p.setChapters(chapters); + return chapters; } Log.i(TAG, "Chapters loaded"); } catch (IOException | ID3ReaderException e) { @@ -126,6 +134,7 @@ public class ChapterUtils { } finally { IOUtils.closeQuietly(in); } + return null; } @NonNull @@ -147,45 +156,52 @@ public class ChapterUtils { return chapters; } - private static void readOggChaptersFromPlayableStreamUrl(Playable media) { + private static List<Chapter> readOggChaptersFromPlayableStreamUrl(Playable media, Context context) { if (media == null || !media.streamAvailable()) { - return; + return null; } InputStream input = null; try { - URL url = new URL(media.getStreamUrl()); - URLConnection urlConnection = url.openConnection(); - urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT); - input = urlConnection.getInputStream(); + if (media.getStreamUrl().startsWith(ContentResolver.SCHEME_CONTENT)) { + Uri uri = Uri.parse(media.getStreamUrl()); + input = context.getContentResolver().openInputStream(uri); + } else { + URL url = new URL(media.getStreamUrl()); + URLConnection urlConnection = url.openConnection(); + urlConnection.setRequestProperty("User-Agent", ClientConfig.USER_AGENT); + input = urlConnection.getInputStream(); + } if (input != null) { - readOggChaptersFromInputStream(media, input); + return readOggChaptersFromInputStream(media, input); } - } catch (IOException e) { + } catch (IOException | IllegalArgumentException e) { Log.e(TAG, Log.getStackTraceString(e)); } finally { IOUtils.closeQuietly(input); } + return null; } - private static void readOggChaptersFromPlayableFileUrl(Playable media) { + private static List<Chapter> readOggChaptersFromPlayableFileUrl(Playable media) { if (media == null || media.getLocalMediaUrl() == null) { - return; + return null; } File source = new File(media.getLocalMediaUrl()); if (source.exists()) { InputStream input = null; try { input = new BufferedInputStream(new FileInputStream(source)); - readOggChaptersFromInputStream(media, input); + return readOggChaptersFromInputStream(media, input); } catch (FileNotFoundException e) { Log.e(TAG, Log.getStackTraceString(e)); } finally { IOUtils.closeQuietly(input); } } + return null; } - private static void readOggChaptersFromInputStream(Playable p, InputStream input) { + private static List<Chapter> readOggChaptersFromInputStream(Playable p, InputStream input) { Log.d(TAG, "Trying to read chapters from item with title " + p.getEpisodeTitle()); try { VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); @@ -193,19 +209,20 @@ public class ChapterUtils { List<Chapter> chapters = reader.getChapters(); if (chapters == null) { Log.i(TAG, "ChapterReader could not find any Ogg vorbis chapters"); - return; + return null; } Collections.sort(chapters, new ChapterStartTimeComparator()); enumerateEmptyChapterTitles(chapters); if (chaptersValid(chapters)) { - p.setChapters(chapters); Log.i(TAG, "Chapters loaded"); + return chapters; } else { Log.e(TAG, "Chapter data was invalid"); } } catch (VorbisCommentReaderException e) { e.printStackTrace(); } + return null; } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java index 220a783f3..2a387b7b0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java @@ -4,6 +4,7 @@ import android.text.TextUtils; import androidx.annotation.VisibleForTesting; import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; import java.io.UnsupportedEncodingException; import java.security.NoSuchAlgorithmException; @@ -29,6 +30,7 @@ public class FileNameGenerator { * characters of the given string. */ public static String generateFileName(String string) { + string = StringUtils.stripAccents(string); StringBuilder buf = new StringBuilder(); for (int i = 0; i < string.length(); i++) { char c = string.charAt(i); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java index a9e46e42c..8cca2f28f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java @@ -2,9 +2,12 @@ package de.danoeh.antennapod.core.util; import android.content.Context; import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; +import android.os.Build; import androidx.core.net.ConnectivityManagerCompat; import android.text.TextUtils; import android.util.Log; @@ -29,65 +32,65 @@ import okhttp3.Response; public class NetworkUtils { private NetworkUtils(){} - private static final String TAG = NetworkUtils.class.getSimpleName(); - - private static Context context; - - public static void init(Context context) { - NetworkUtils.context = context; - } - - /** - * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for - * automatic downloads is disabled or the device is connected to a Wi-Fi - * network that is on the 'selected networks' list of the Wi-Fi filter for - * automatic downloads and false otherwise. - * */ - public static boolean autodownloadNetworkAvailable() { - ConnectivityManager cm = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo networkInfo = cm.getActiveNetworkInfo(); - if (networkInfo != null) { - if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { - Log.d(TAG, "Device is connected to Wi-Fi"); - if (networkInfo.isConnected()) { - if (!UserPreferences.isEnableAutodownloadWifiFilter()) { - Log.d(TAG, "Auto-dl filter is disabled"); - return true; - } else { - WifiManager wm = (WifiManager) context.getApplicationContext() - .getSystemService(Context.WIFI_SERVICE); - WifiInfo wifiInfo = wm.getConnectionInfo(); - List<String> selectedNetworks = Arrays - .asList(UserPreferences - .getAutodownloadSelectedNetworks()); - if (selectedNetworks.contains(Integer.toString(wifiInfo - .getNetworkId()))) { - Log.d(TAG, "Current network is on the selected networks list"); - return true; - } - } - } - } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) { - Log.d(TAG, "Device is connected to Ethernet"); - if (networkInfo.isConnected()) { - return true; - } - } else { - if (!UserPreferences.isAllowMobileAutoDownload()) { - Log.d(TAG, "Auto Download not enabled on Mobile"); - return false; - } - if (networkInfo.isRoaming()) { - Log.d(TAG, "Roaming on foreign network"); - return false; - } - return true; - } - } - Log.d(TAG, "Network for auto-dl is not available"); - return false; - } + private static final String TAG = NetworkUtils.class.getSimpleName(); + + private static Context context; + + public static void init(Context context) { + NetworkUtils.context = context; + } + + /** + * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for + * automatic downloads is disabled or the device is connected to a Wi-Fi + * network that is on the 'selected networks' list of the Wi-Fi filter for + * automatic downloads and false otherwise. + * */ + public static boolean autodownloadNetworkAvailable() { + ConnectivityManager cm = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + if (networkInfo != null) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + Log.d(TAG, "Device is connected to Wi-Fi"); + if (networkInfo.isConnected()) { + if (!UserPreferences.isEnableAutodownloadWifiFilter()) { + Log.d(TAG, "Auto-dl filter is disabled"); + return true; + } else { + WifiManager wm = (WifiManager) context.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wm.getConnectionInfo(); + List<String> selectedNetworks = Arrays + .asList(UserPreferences + .getAutodownloadSelectedNetworks()); + if (selectedNetworks.contains(Integer.toString(wifiInfo + .getNetworkId()))) { + Log.d(TAG, "Current network is on the selected networks list"); + return true; + } + } + } + } else if (networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET) { + Log.d(TAG, "Device is connected to Ethernet"); + if (networkInfo.isConnected()) { + return true; + } + } else { + if (!UserPreferences.isAllowMobileAutoDownload()) { + Log.d(TAG, "Auto Download not enabled on Mobile"); + return false; + } + if (networkInfo.isRoaming()) { + Log.d(TAG, "Roaming on foreign network"); + return false; + } + return true; + } + } + Log.d(TAG, "Network for auto-dl is not available"); + return false; + } public static boolean networkAvailable() { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); @@ -96,7 +99,7 @@ public class NetworkUtils { } public static boolean isEpisodeDownloadAllowed() { - return UserPreferences.isAllowMobileEpisodeDownload() || !NetworkUtils.isNetworkMetered(); + return UserPreferences.isAllowMobileEpisodeDownload() || !NetworkUtils.isNetworkRestricted(); } public static boolean isEpisodeHeadDownloadAllowed() { @@ -106,22 +109,53 @@ public class NetworkUtils { } public static boolean isImageAllowed() { - return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkMetered(); + return UserPreferences.isAllowMobileImages() || !NetworkUtils.isNetworkRestricted(); } public static boolean isStreamingAllowed() { - return UserPreferences.isAllowMobileStreaming() || !NetworkUtils.isNetworkMetered(); + return UserPreferences.isAllowMobileStreaming() || !NetworkUtils.isNetworkRestricted(); } public static boolean isFeedRefreshAllowed() { - return UserPreferences.isAllowMobileFeedRefresh() || !NetworkUtils.isNetworkMetered(); + return UserPreferences.isAllowMobileFeedRefresh() || !NetworkUtils.isNetworkRestricted(); + } + + public static boolean isNetworkRestricted() { + return isNetworkMetered() || isNetworkCellular(); } - private static boolean isNetworkMetered() { - ConnectivityManager connManager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); + private static boolean isNetworkMetered() { + ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); return ConnectivityManagerCompat.isActiveNetworkMetered(connManager); - } + } + + private static boolean isNetworkCellular() { + ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (Build.VERSION.SDK_INT >= 23) { + Network network = connManager.getActiveNetwork(); + if (network == null) { + return false; // Nothing connected + } + NetworkInfo info = connManager.getNetworkInfo(network); + if (info == null) { + return true; // Better be safe than sorry + } + NetworkCapabilities capabilities = connManager.getNetworkCapabilities(network); + if (capabilities == null) { + return true; // Better be safe than sorry + } + return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); + } else { + // if the default network is a VPN, + // this method will return the NetworkInfo for one of its underlying networks + NetworkInfo info = connManager.getActiveNetworkInfo(); + if (info == null) { + return false; // Nothing connected + } + //noinspection deprecation + return info.getType() == ConnectivityManager.TYPE_MOBILE; + } + } /** * Returns the SSID of the wifi connection, or <code>null</code> if there is no wifi. @@ -135,7 +169,7 @@ public class NetworkUtils { return null; } - public static Single<Long> getFeedMediaSizeObservable(FeedMedia media) { + public static Single<Long> getFeedMediaSizeObservable(FeedMedia media) { return Single.create((SingleOnSubscribe<Long>) emitter -> { if (!NetworkUtils.isEpisodeHeadDownloadAllowed()) { emitter.onSuccess(0L); @@ -188,7 +222,7 @@ public class NetworkUtils { DBWriter.setFeedMedia(media); }) .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + .observeOn(AndroidSchedulers.mainThread()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java index 366f86707..813c6d0f7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java @@ -39,7 +39,7 @@ public class RewindAfterPauseUtils { int newPosition = currentPosition - (int) rewindTime; - return newPosition > 0 ? newPosition : 0; + return Math.max(newPosition, 0); } else { return currentPosition; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java index 8bd23c2ed..920a1ef8a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java @@ -8,13 +8,7 @@ public class ChapterStartTimeComparator implements Comparator<Chapter> { @Override public int compare(Chapter lhs, Chapter rhs) { - if (lhs.getStart() == rhs.getStart()) { - return 0; - } else if (lhs.getStart() < rhs.getStart()) { - return -1; - } else { - return 1; - } + return Long.compare(lhs.getStart(), rhs.getStart()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java index 51fe2da78..ad81a1d17 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java @@ -4,16 +4,20 @@ import java.util.Comparator; import de.danoeh.antennapod.core.feed.FeedItem; -/** Compares the pubDate of two FeedItems for sorting*/ +/** + * Compares the pubDate of two FeedItems for sorting. + */ public class FeedItemPubdateComparator implements Comparator<FeedItem> { - /** Returns a new instance of this comparator in reverse order. - public static FeedItemPubdateComparator newInstance() { - FeedItemPubdateComparator - }*/ - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - return rhs.getPubDate().compareTo(lhs.getPubDate()); - } + /** + * Returns a new instance of this comparator in reverse order. + */ + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + if (rhs.getPubDate() == null || lhs.getPubDate() == null) { + return 0; + } + return rhs.getPubDate().compareTo(lhs.getPubDate()); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java index 991089910..a8ca43ccb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/AutoUpdateManager.java @@ -118,9 +118,8 @@ public class AutoUpdateManager { */ public static void runImmediate(@NonNull Context context) { Log.d(TAG, "Run auto update immediately in background."); - new Thread(() -> { - DBTasks.refreshAllFeeds(context.getApplicationContext(), true); - }, "ManualRefreshAllFeeds").start(); + new Thread(() -> DBTasks.refreshAllFeeds( + context.getApplicationContext(), true), "ManualRefreshAllFeeds").start(); } public static void disableAutoUpdate(Context context) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java index f546ca019..ddbe68938 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java @@ -14,6 +14,7 @@ public class NotificationUtils { public static final String CHANNEL_ID_DOWNLOADING = "downloading"; public static final String CHANNEL_ID_PLAYING = "playing"; public static final String CHANNEL_ID_ERROR = "error"; + public static final String CHANNEL_ID_SYNC_ERROR = "sync_error"; public static final String CHANNEL_ID_AUTO_DOWNLOAD = "auto_download"; public static void createChannels(Context context) { @@ -27,6 +28,7 @@ public class NotificationUtils { mNotificationManager.createNotificationChannel(createChannelDownloading(context)); mNotificationManager.createNotificationChannel(createChannelPlaying(context)); mNotificationManager.createNotificationChannel(createChannelError(context)); + mNotificationManager.createNotificationChannel(createChannelSyncError(context)); mNotificationManager.createNotificationChannel(createChannelAutoDownload(context)); } } @@ -66,6 +68,14 @@ public class NotificationUtils { } @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannel createChannelSyncError(Context c) { + NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_SYNC_ERROR, + c.getString(R.string.notification_channel_sync_error), NotificationManager.IMPORTANCE_HIGH); + notificationChannel.setDescription(c.getString(R.string.notification_channel_sync_error_description)); + return notificationChannel; + } + + @RequiresApi(api = Build.VERSION_CODES.O) private static NotificationChannel createChannelAutoDownload(Context c) { NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_AUTO_DOWNLOAD, c.getString(R.string.notification_channel_auto_download), NotificationManager.IMPORTANCE_DEFAULT); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java index aec53da4c..fecb14d25 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import androidx.preference.PreferenceManager; import android.util.Log; import android.view.SurfaceHolder; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java index b55091009..6c107996f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java @@ -99,11 +99,11 @@ public class ExternalMedia implements Playable { e.printStackTrace(); throw new PlayableException("NumberFormatException when reading duration of media file"); } - ChapterUtils.loadChaptersFromFileUrl(this); + setChapters(ChapterUtils.loadChaptersFromFileUrl(this)); } @Override - public void loadChapterMarks() { + public void loadChapterMarks(Context context) { } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java index 24aabf212..5b15913c8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -3,12 +3,9 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.content.SharedPreferences; import android.os.Parcelable; -import android.preference.PreferenceManager; -import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; import android.util.Log; - -import java.util.List; - +import androidx.annotation.Nullable; import de.danoeh.antennapod.core.asynctask.ImageResource; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -17,6 +14,8 @@ import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.ShownotesProvider; +import java.util.List; + /** * Interface for objects that can be played by the PlaybackService. */ @@ -44,7 +43,7 @@ public interface Playable extends Parcelable, * Playable objects should load their chapter marks in this method if no * local file was available when loadMetadata() was called. */ - void loadChapterMarks(); + void loadChapterMarks(Context context); /** * Returns the title of the episode that this playable represents diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index 77525e1e5..425a07f4a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -600,6 +600,13 @@ public class PlaybackController { } public void setPlaybackSpeed(float speed) { + PlaybackPreferences.setCurrentlyPlayingTemporaryPlaybackSpeed(speed); + if (getMedia() != null && getMedia().getMediaType() == MediaType.VIDEO) { + UserPreferences.setVideoPlaybackSpeed(speed); + } else { + UserPreferences.setPlaybackSpeed(speed); + } + if (playbackService != null) { playbackService.setSpeed(speed); } else { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java index ca09cda4b..29eb20aca 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/RemoteMedia.java @@ -129,8 +129,8 @@ public class RemoteMedia implements Playable { } @Override - public void loadChapterMarks() { - ChapterUtils.loadChaptersFromStreamUrl(this); + public void loadChapterMarks(Context context) { + setChapters(ChapterUtils.loadChaptersFromStreamUrl(this, context)); } @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java index cdf171299..9277af6e6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java @@ -39,7 +39,7 @@ class OggInputStream extends InputStream { private void readOggPage() throws IOException { // find OggS int[] buffer = new int[4]; - int c = 0; + int c; boolean isInOggS = false; while ((c = input.read()) != -1) { switch (c) { |