summaryrefslogtreecommitdiff
path: root/net/download/service
diff options
context:
space:
mode:
Diffstat (limited to 'net/download/service')
-rw-r--r--net/download/service/build.gradle3
-rw-r--r--net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APCleanupAlgorithm.java135
-rw-r--r--net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APNullCleanupAlgorithm.java29
-rw-r--r--net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APQueueCleanupAlgorithm.java99
-rw-r--r--net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/AutoDownloadManagerImpl.java55
-rw-r--r--net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/AutomaticDownloadAlgorithm.java121
-rw-r--r--net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/EpisodeCleanupAlgorithm.java66
-rw-r--r--net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/EpisodeCleanupAlgorithmFactory.java22
-rw-r--r--net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/ExceptFavoriteCleanupAlgorithm.java104
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APCleanupAlgorithmTest.java20
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbCleanupTests.java234
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbNullCleanupAlgorithmTest.java125
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbQueueCleanupAlgorithmTest.java54
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbReaderTest.java526
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbTasksTest.java249
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbTestUtils.java74
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbWriterTest.java832
-rw-r--r--net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/ExceptFavoriteCleanupAlgorithmTest.java91
18 files changed, 2838 insertions, 1 deletions
diff --git a/net/download/service/build.gradle b/net/download/service/build.gradle
index 75d6b26de..789fb3aef 100644
--- a/net/download/service/build.gradle
+++ b/net/download/service/build.gradle
@@ -43,5 +43,6 @@ dependencies {
testImplementation "junit:junit:$junitVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
- testImplementation 'org.mockito:mockito-core:5.11.0'
+ testImplementation "org.mockito:mockito-core:$mockitoVersion"
+ testImplementation "androidx.preference:preference:$preferenceVersion"
}
diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APCleanupAlgorithm.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APCleanupAlgorithm.java
new file mode 100644
index 000000000..bc50c8c1f
--- /dev/null
+++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APCleanupAlgorithm.java
@@ -0,0 +1,135 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.database.DBWriter;
+
+/**
+ * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod.
+ */
+public class APCleanupAlgorithm extends EpisodeCleanupAlgorithm {
+
+ private static final String TAG = "APCleanupAlgorithm";
+ /** the number of days after playback to wait before an item is eligible to be cleaned up.
+ Fractional for number of hours, e.g., 0.5 = 12 hours, 0.0416 = 1 hour. */
+ private final int numberOfHoursAfterPlayback;
+
+ public APCleanupAlgorithm(int numberOfHoursAfterPlayback) {
+ this.numberOfHoursAfterPlayback = numberOfHoursAfterPlayback;
+ }
+
+ /**
+ * @return the number of episodes that *could* be cleaned up, if needed
+ */
+ public int getReclaimableItems()
+ {
+ return getCandidates().size();
+ }
+
+ @Override
+ public int performCleanup(Context context, int numberOfEpisodesToDelete) {
+ List<FeedItem> candidates = getCandidates();
+ List<FeedItem> delete;
+
+ Collections.sort(candidates, (lhs, rhs) -> {
+ Date l = lhs.getMedia().getPlaybackCompletionDate();
+ Date r = rhs.getMedia().getPlaybackCompletionDate();
+
+ if (l == null) {
+ l = new Date();
+ }
+ if (r == null) {
+ r = new Date();
+ }
+ return l.compareTo(r);
+ });
+
+ if (candidates.size() > numberOfEpisodesToDelete) {
+ delete = candidates.subList(0, numberOfEpisodesToDelete);
+ } else {
+ delete = candidates;
+ }
+
+ for (FeedItem item : delete) {
+ try {
+ DBWriter.deleteFeedMediaOfItem(context, item.getMedia()).get();
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ int counter = delete.size();
+
+
+ Log.i(TAG, String.format(Locale.US,
+ "Auto-delete deleted %d episodes (%d requested)", counter,
+ numberOfEpisodesToDelete));
+
+ return counter;
+ }
+
+ @VisibleForTesting
+ Date calcMostRecentDateForDeletion(@NonNull Date currentDate) {
+ return minusHours(currentDate, numberOfHoursAfterPlayback);
+ }
+
+ @NonNull
+ private List<FeedItem> getCandidates() {
+ List<FeedItem> candidates = new ArrayList<>();
+ List<FeedItem> downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE,
+ new FeedItemFilter(FeedItemFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD);
+
+ Date mostRecentDateForDeletion = calcMostRecentDateForDeletion(new Date());
+ for (FeedItem item : downloadedItems) {
+ if (item.hasMedia()
+ && item.getMedia().isDownloaded()
+ && !item.isTagged(FeedItem.TAG_QUEUE)
+ && item.isPlayed()
+ && !item.isTagged(FeedItem.TAG_FAVORITE)) {
+ FeedMedia media = item.getMedia();
+ // make sure this candidate was played at least the proper amount of days prior
+ // to now
+ if (media != null
+ && media.getPlaybackCompletionDate() != null
+ && media.getPlaybackCompletionDate().before(mostRecentDateForDeletion)) {
+ candidates.add(item);
+ }
+ }
+ }
+ return candidates;
+ }
+
+ @Override
+ public int getDefaultCleanupParameter() {
+ return getNumEpisodesToCleanup(0);
+ }
+
+ @VisibleForTesting
+ public int getNumberOfHoursAfterPlayback() { return numberOfHoursAfterPlayback; }
+
+ private static Date minusHours(Date baseDate, int numberOfHours) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(baseDate);
+
+ cal.add(Calendar.HOUR_OF_DAY, -1 * numberOfHours);
+
+ return cal.getTime();
+ }
+
+}
diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APNullCleanupAlgorithm.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APNullCleanupAlgorithm.java
new file mode 100644
index 000000000..f550cecf8
--- /dev/null
+++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APNullCleanupAlgorithm.java
@@ -0,0 +1,29 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * A cleanup algorithm that never removes anything
+ */
+public class APNullCleanupAlgorithm extends EpisodeCleanupAlgorithm {
+
+ private static final String TAG = "APNullCleanupAlgorithm";
+
+ @Override
+ public int performCleanup(Context context, int parameter) {
+ // never clean anything up
+ Log.i(TAG, "performCleanup: Not removing anything");
+ return 0;
+ }
+
+ @Override
+ public int getDefaultCleanupParameter() {
+ return 0;
+ }
+
+ @Override
+ public int getReclaimableItems() {
+ return 0;
+ }
+}
diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APQueueCleanupAlgorithm.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APQueueCleanupAlgorithm.java
new file mode 100644
index 000000000..ea550599b
--- /dev/null
+++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APQueueCleanupAlgorithm.java
@@ -0,0 +1,99 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.database.DBWriter;
+
+/**
+ * A cleanup algorithm that removes any item that isn't in the queue and isn't a favorite
+ * but only if space is needed.
+ */
+public class APQueueCleanupAlgorithm extends EpisodeCleanupAlgorithm {
+
+ private static final String TAG = "APQueueCleanupAlgorithm";
+
+ /**
+ * @return the number of episodes that *could* be cleaned up, if needed
+ */
+ public int getReclaimableItems()
+ {
+ return getCandidates().size();
+ }
+
+ @Override
+ public int performCleanup(Context context, int numberOfEpisodesToDelete) {
+ List<FeedItem> candidates = getCandidates();
+ List<FeedItem> delete;
+
+ // in the absence of better data, we'll sort by item publication date
+ Collections.sort(candidates, (lhs, rhs) -> {
+ Date l = lhs.getPubDate();
+ Date r = rhs.getPubDate();
+
+ if (l == null) {
+ l = new Date();
+ }
+ if (r == null) {
+ r = new Date();
+ }
+ return l.compareTo(r);
+ });
+
+ if (candidates.size() > numberOfEpisodesToDelete) {
+ delete = candidates.subList(0, numberOfEpisodesToDelete);
+ } else {
+ delete = candidates;
+ }
+
+ for (FeedItem item : delete) {
+ try {
+ DBWriter.deleteFeedMediaOfItem(context, item.getMedia()).get();
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ int counter = delete.size();
+
+
+ Log.i(TAG, String.format(Locale.US,
+ "Auto-delete deleted %d episodes (%d requested)", counter,
+ numberOfEpisodesToDelete));
+
+ return counter;
+ }
+
+ @NonNull
+ private List<FeedItem> getCandidates() {
+ List<FeedItem> candidates = new ArrayList<>();
+ List<FeedItem> downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE,
+ new FeedItemFilter(FeedItemFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD);
+ for (FeedItem item : downloadedItems) {
+ if (item.hasMedia()
+ && item.getMedia().isDownloaded()
+ && !item.isTagged(FeedItem.TAG_QUEUE)
+ && !item.isTagged(FeedItem.TAG_FAVORITE)) {
+ candidates.add(item);
+ }
+ }
+ return candidates;
+ }
+
+ @Override
+ public int getDefaultCleanupParameter() {
+ return getNumEpisodesToCleanup(0);
+ }
+}
diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/AutoDownloadManagerImpl.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/AutoDownloadManagerImpl.java
new file mode 100644
index 000000000..2b0eb4d62
--- /dev/null
+++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/AutoDownloadManagerImpl.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import android.util.Log;
+import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+public class AutoDownloadManagerImpl extends AutoDownloadManager {
+ private static final String TAG = "AutoDownloadManager";
+
+ /**
+ * Executor service used by the autodownloadUndownloadedEpisodes method.
+ */
+ private static final ExecutorService autodownloadExec;
+
+ private static AutomaticDownloadAlgorithm downloadAlgorithm = new AutomaticDownloadAlgorithm();
+
+ static {
+ autodownloadExec = Executors.newSingleThreadExecutor(r -> {
+ Thread t = new Thread(r);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ });
+ }
+
+ /**
+ * Looks for non-downloaded episodes in the queue or list of unread items and request a download if
+ * 1. Network is available
+ * 2. The device is charging or the user allows auto download on battery
+ * 3. There is free space in the episode cache
+ * This method is executed on an internal single thread executor.
+ *
+ * @param context Used for accessing the DB.
+ * @return A Future that can be used for waiting for the methods completion.
+ */
+ public Future<?> autodownloadUndownloadedItems(final Context context) {
+ Log.d(TAG, "autodownloadUndownloadedItems");
+ return autodownloadExec.submit(downloadAlgorithm.autoDownloadUndownloadedItems(context));
+ }
+
+ /**
+ * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller
+ * 'playbackCompletionDate'-value will be deleted first.
+ * <p/>
+ * This method should NOT be executed on the GUI thread.
+ *
+ * @param context Used for accessing the DB.
+ */
+ public void performAutoCleanup(final Context context) {
+ EpisodeCleanupAlgorithmFactory.build().performCleanup(context);
+ }
+}
diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/AutomaticDownloadAlgorithm.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/AutomaticDownloadAlgorithm.java
new file mode 100644
index 000000000..828211ba1
--- /dev/null
+++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/AutomaticDownloadAlgorithm.java
@@ -0,0 +1,121 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedPreferences;
+import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.net.common.NetworkUtils;
+
+/**
+ * Implements the automatic download algorithm used by AntennaPod. This class assumes that
+ * the client uses the {@link EpisodeCleanupAlgorithm}.
+ */
+public class AutomaticDownloadAlgorithm {
+ private static final String TAG = "DownloadAlgorithm";
+
+ /**
+ * Looks for undownloaded episodes in the queue or list of new items and request a download if
+ * 1. Network is available
+ * 2. The device is charging or the user allows auto download on battery
+ * 3. There is free space in the episode cache
+ * This method is executed on an internal single thread executor.
+ *
+ * @param context Used for accessing the DB.
+ * @return A Runnable that will be submitted to an ExecutorService.
+ */
+ public Runnable autoDownloadUndownloadedItems(final Context context) {
+ return () -> {
+
+ // true if we should auto download based on network status
+ boolean networkShouldAutoDl = NetworkUtils.isAutoDownloadAllowed()
+ && UserPreferences.isEnableAutodownload();
+
+ // true if we should auto download based on power status
+ boolean powerShouldAutoDl = deviceCharging(context) || UserPreferences.isEnableAutodownloadOnBattery();
+
+ // we should only auto download if both network AND power are happy
+ if (networkShouldAutoDl && powerShouldAutoDl) {
+
+ Log.d(TAG, "Performing auto-dl of undownloaded episodes");
+
+ List<FeedItem> candidates;
+ final List<FeedItem> queue = DBReader.getQueue();
+ final List<FeedItem> newItems = DBReader.getEpisodes(0, Integer.MAX_VALUE,
+ new FeedItemFilter(FeedItemFilter.NEW), SortOrder.DATE_NEW_OLD);
+ candidates = new ArrayList<>(queue.size() + newItems.size());
+ candidates.addAll(queue);
+ for (FeedItem newItem : newItems) {
+ FeedPreferences feedPrefs = newItem.getFeed().getPreferences();
+ if (feedPrefs.getAutoDownload()
+ && !candidates.contains(newItem)
+ && feedPrefs.getFilter().shouldAutoDownload(newItem)) {
+ candidates.add(newItem);
+ }
+ }
+
+ // filter items that are not auto downloadable
+ Iterator<FeedItem> it = candidates.iterator();
+ while (it.hasNext()) {
+ FeedItem item = it.next();
+ if (!item.isAutoDownloadEnabled()
+ || item.isDownloaded()
+ || !item.hasMedia()
+ || item.getFeed().isLocalFeed()) {
+ it.remove();
+ }
+ }
+
+ int autoDownloadableEpisodes = candidates.size();
+ int downloadedEpisodes = DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED));
+ int deletedEpisodes = EpisodeCleanupAlgorithmFactory.build()
+ .makeRoomForEpisodes(context, autoDownloadableEpisodes);
+ boolean cacheIsUnlimited =
+ UserPreferences.getEpisodeCacheSize() == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED;
+ int episodeCacheSize = UserPreferences.getEpisodeCacheSize();
+
+ int episodeSpaceLeft;
+ if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) {
+ episodeSpaceLeft = autoDownloadableEpisodes;
+ } else {
+ episodeSpaceLeft = episodeCacheSize - (downloadedEpisodes - deletedEpisodes);
+ }
+
+ List<FeedItem> itemsToDownload = candidates.subList(0, episodeSpaceLeft);
+ if (itemsToDownload.size() > 0) {
+ Log.d(TAG, "Enqueueing " + itemsToDownload.size() + " items for download");
+
+ for (FeedItem episode : itemsToDownload) {
+ DownloadServiceInterface.get().download(context, episode);
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * @return true if the device is charging
+ */
+ public static boolean deviceCharging(Context context) {
+ // from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html
+ IntentFilter intentFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
+ Intent batteryStatus = context.registerReceiver(null, intentFilter);
+
+ int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
+ return (status == BatteryManager.BATTERY_STATUS_CHARGING
+ || status == BatteryManager.BATTERY_STATUS_FULL);
+
+ }
+}
diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/EpisodeCleanupAlgorithm.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/EpisodeCleanupAlgorithm.java
new file mode 100644
index 000000000..eb582a19a
--- /dev/null
+++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/EpisodeCleanupAlgorithm.java
@@ -0,0 +1,66 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+public abstract class EpisodeCleanupAlgorithm {
+
+ /**
+ * Deletes downloaded episodes that are no longer needed. What episodes are deleted and how many
+ * of them depends on the implementation.
+ *
+ * @param context Can be used for accessing the database
+ * @param numToRemove An additional parameter. This parameter is either returned by getDefaultCleanupParameter
+ * or getPerformCleanupParameter.
+ * @return The number of episodes that were deleted.
+ */
+ protected abstract int performCleanup(Context context, int numToRemove);
+
+ public int performCleanup(Context context) {
+ return performCleanup(context, getDefaultCleanupParameter());
+ }
+
+ /**
+ * Returns a parameter for performCleanup. The implementation of this interface should decide how much
+ * space to free to satisfy the episode cache conditions. If the conditions are already satisfied, this
+ * method should not have any effects.
+ */
+ protected abstract int getDefaultCleanupParameter();
+
+ /**
+ * Cleans up just enough episodes to make room for the requested number
+ *
+ * @param context Can be used for accessing the database
+ * @param amountOfRoomNeeded the number of episodes we need space for
+ * @return The number of epiosdes that were deleted
+ */
+ public int makeRoomForEpisodes(Context context, int amountOfRoomNeeded) {
+ return performCleanup(context, getNumEpisodesToCleanup(amountOfRoomNeeded));
+ }
+
+ /**
+ * @return the number of episodes/items that *could* be cleaned up, if needed
+ */
+ public abstract int getReclaimableItems();
+
+ /**
+ * @param amountOfRoomNeeded the number of episodes we want to download
+ * @return the number of episodes to delete in order to make room
+ */
+ int getNumEpisodesToCleanup(final int amountOfRoomNeeded) {
+ if (amountOfRoomNeeded >= 0
+ && UserPreferences.getEpisodeCacheSize() != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
+ int downloadedEpisodes = DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED));
+ if (downloadedEpisodes + amountOfRoomNeeded >= UserPreferences
+ .getEpisodeCacheSize()) {
+
+ return downloadedEpisodes + amountOfRoomNeeded
+ - UserPreferences.getEpisodeCacheSize();
+ }
+ }
+ return 0;
+ }
+}
diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/EpisodeCleanupAlgorithmFactory.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/EpisodeCleanupAlgorithmFactory.java
new file mode 100644
index 000000000..de8a2feda
--- /dev/null
+++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/EpisodeCleanupAlgorithmFactory.java
@@ -0,0 +1,22 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+public abstract class EpisodeCleanupAlgorithmFactory {
+ public static EpisodeCleanupAlgorithm build() {
+ if (!UserPreferences.isEnableAutodownload()) {
+ return new APNullCleanupAlgorithm();
+ }
+ int cleanupValue = UserPreferences.getEpisodeCleanupValue();
+ switch (cleanupValue) {
+ case UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE:
+ return new ExceptFavoriteCleanupAlgorithm();
+ case UserPreferences.EPISODE_CLEANUP_QUEUE:
+ return new APQueueCleanupAlgorithm();
+ case UserPreferences.EPISODE_CLEANUP_NULL:
+ return new APNullCleanupAlgorithm();
+ default:
+ return new APCleanupAlgorithm(cleanupValue);
+ }
+ }
+}
diff --git a/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/ExceptFavoriteCleanupAlgorithm.java b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/ExceptFavoriteCleanupAlgorithm.java
new file mode 100644
index 000000000..46dfcffdc
--- /dev/null
+++ b/net/download/service/src/main/java/de/danoeh/antennapod/net/download/service/episode/autodownload/ExceptFavoriteCleanupAlgorithm.java
@@ -0,0 +1,104 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.database.DBWriter;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+/**
+ * A cleanup algorithm that removes any item that isn't a favorite but only if space is needed.
+ */
+public class ExceptFavoriteCleanupAlgorithm extends EpisodeCleanupAlgorithm {
+
+ private static final String TAG = "ExceptFavCleanupAlgo";
+
+ /**
+ * The maximum number of episodes that could be cleaned up.
+ *
+ * @return the number of episodes that *could* be cleaned up, if needed
+ */
+ public int getReclaimableItems() {
+ return getCandidates().size();
+ }
+
+ @Override
+ public int performCleanup(Context context, int numberOfEpisodesToDelete) {
+ List<FeedItem> candidates = getCandidates();
+ List<FeedItem> delete;
+
+ // in the absence of better data, we'll sort by item publication date
+ Collections.sort(candidates, (lhs, rhs) -> {
+ Date l = lhs.getPubDate();
+ Date r = rhs.getPubDate();
+
+ if (l != null && r != null) {
+ return l.compareTo(r);
+ } else {
+ // No date - compare by id which should be always incremented
+ return Long.compare(lhs.getId(), rhs.getId());
+ }
+ });
+
+ if (candidates.size() > numberOfEpisodesToDelete) {
+ delete = candidates.subList(0, numberOfEpisodesToDelete);
+ } else {
+ delete = candidates;
+ }
+
+ for (FeedItem item : delete) {
+ try {
+ DBWriter.deleteFeedMediaOfItem(context, item.getMedia()).get();
+ } catch (InterruptedException | ExecutionException e) {
+ e.printStackTrace();
+ }
+ }
+
+ int counter = delete.size();
+ Log.i(TAG, String.format(Locale.US,
+ "Auto-delete deleted %d episodes (%d requested)", counter,
+ numberOfEpisodesToDelete));
+
+ return counter;
+ }
+
+ @NonNull
+ private List<FeedItem> getCandidates() {
+ List<FeedItem> candidates = new ArrayList<>();
+ List<FeedItem> downloadedItems = DBReader.getEpisodes(0, Integer.MAX_VALUE,
+ new FeedItemFilter(FeedItemFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD);
+ for (FeedItem item : downloadedItems) {
+ if (item.hasMedia()
+ && item.getMedia().isDownloaded()
+ && !item.isTagged(FeedItem.TAG_FAVORITE)) {
+ candidates.add(item);
+ }
+ }
+ return candidates;
+ }
+
+ @Override
+ public int getDefaultCleanupParameter() {
+ int cacheSize = UserPreferences.getEpisodeCacheSize();
+ if (cacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
+ int downloadedEpisodes = DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.DOWNLOADED));
+ if (downloadedEpisodes > cacheSize) {
+ return downloadedEpisodes - cacheSize;
+ }
+ }
+ return 0;
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APCleanupAlgorithmTest.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APCleanupAlgorithmTest.java
new file mode 100644
index 000000000..0072e7ac0
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/APCleanupAlgorithmTest.java
@@ -0,0 +1,20 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import org.junit.Test;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import static org.junit.Assert.assertEquals;
+
+public class APCleanupAlgorithmTest {
+
+ @Test
+ public void testCalcMostRecentDateForDeletion() throws Exception {
+ APCleanupAlgorithm algo = new APCleanupAlgorithm(24);
+ Date curDateForTest = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse("2018-11-13T14:08:56-0800");
+ Date resExpected = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse("2018-11-12T14:08:56-0800");
+ Date resActual = algo.calcMostRecentDateForDeletion(curDateForTest);
+ assertEquals("cutoff for retaining most recent 1 day", resExpected, resActual);
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbCleanupTests.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbCleanupTests.java
new file mode 100644
index 000000000..dc9c8749a
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbCleanupTests.java
@@ -0,0 +1,234 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import androidx.preference.PreferenceManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager;
+import de.danoeh.antennapod.storage.database.DBWriter;
+import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.storage.preferences.SynchronizationSettings;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import static de.danoeh.antennapod.net.download.service.episode.autodownload.DbTestUtils.saveFeedlist;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for DBTasks.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class DbCleanupTests {
+
+ static final int EPISODE_CACHE_SIZE = 5;
+ private int cleanupAlgorithm;
+
+ Context context;
+
+ private File destFolder;
+
+ public DbCleanupTests() {
+ setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_DEFAULT);
+ }
+
+ protected void setCleanupAlgorithm(int cleanupAlgorithm) {
+ this.cleanupAlgorithm = cleanupAlgorithm;
+ }
+
+ @Before
+ public void setUp() {
+ context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ destFolder = new File(context.getCacheDir(), "DbCleanupTests");
+ //noinspection ResultOfMethodCallIgnored
+ destFolder.mkdir();
+ cleanupDestFolder(destFolder);
+ assertNotNull(destFolder);
+ assertTrue(destFolder.exists());
+ assertTrue(destFolder.canWrite());
+
+ // create new database
+ PodDBAdapter.init(context);
+ PodDBAdapter.deleteDatabase();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.close();
+
+ SharedPreferences.Editor prefEdit = PreferenceManager
+ .getDefaultSharedPreferences(context.getApplicationContext()).edit();
+ prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, Integer.toString(EPISODE_CACHE_SIZE));
+ prefEdit.putString(UserPreferences.PREF_EPISODE_CLEANUP, Integer.toString(cleanupAlgorithm));
+ prefEdit.putBoolean(UserPreferences.PREF_ENABLE_AUTODL, true);
+ prefEdit.commit();
+
+ UserPreferences.init(context);
+ PlaybackPreferences.init(context);
+ SynchronizationSettings.init(context);
+ AutoDownloadManager.setInstance(new AutoDownloadManagerImpl());
+ }
+
+ @After
+ public void tearDown() {
+ cleanupDestFolder(destFolder);
+ assertTrue(destFolder.delete());
+
+ DBWriter.tearDownTests();
+ PodDBAdapter.tearDownTests();
+ }
+
+ private void cleanupDestFolder(File destFolder) {
+ //noinspection ConstantConditions
+ for (File f : destFolder.listFiles()) {
+ assertTrue(f.delete());
+ }
+ }
+
+ @Test
+ public void testPerformAutoCleanupShouldDelete() throws IOException {
+ final int numItems = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ populateItems(numItems, feed, items, files, FeedItem.PLAYED, false, false);
+
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (int i = 0; i < files.size(); i++) {
+ if (i < EPISODE_CACHE_SIZE) {
+ assertTrue(files.get(i).exists());
+ } else {
+ assertFalse(files.get(i).exists());
+ }
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ void populateItems(final int numItems, Feed feed, List<FeedItem> items,
+ List<File> files, int itemState, boolean addToQueue,
+ boolean addToFavorites) throws IOException {
+ for (int i = 0; i < numItems; i++) {
+ Date itemDate = new Date(numItems - i);
+ Date playbackCompletionDate = null;
+ if (itemState == FeedItem.PLAYED) {
+ playbackCompletionDate = itemDate;
+ }
+ FeedItem item = new FeedItem(0, "title", "id" + i, "link", itemDate, itemState, feed);
+
+ File f = new File(destFolder, "file " + i);
+ assertTrue(f.createNewFile());
+ files.add(f);
+ item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m",
+ f.getAbsolutePath(), "url", true, playbackCompletionDate, 0, 0));
+ items.add(item);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ if (addToQueue) {
+ adapter.setQueue(items);
+ }
+ if (addToFavorites) {
+ adapter.setFavorites(items);
+ }
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : items) {
+ assertTrue(item.getId() != 0);
+ //noinspection ConstantConditions
+ assertTrue(item.getMedia().getId() != 0);
+ }
+ }
+
+ @Test
+ public void testPerformAutoCleanupHandleUnplayed() throws IOException {
+ final int numItems = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ populateItems(numItems, feed, items, files, FeedItem.UNPLAYED, false, false);
+
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (File file : files) {
+ assertTrue(file.exists());
+ }
+ }
+
+ @Test
+ public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue() throws IOException {
+ final int numItems = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ populateItems(numItems, feed, items, files, FeedItem.PLAYED, true, false);
+
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (File file : files) {
+ assertTrue(file.exists());
+ }
+ }
+
+ /**
+ * Reproduces a bug where DBTasks.performAutoCleanup(android.content.Context) would use the ID
+ * of the FeedItem in the call to DBWriter.deleteFeedMediaOfItem instead of the ID of the FeedMedia.
+ * This would cause the wrong item to be deleted.
+ */
+ @Test
+ public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue_withFeedsWithNoMedia() throws IOException {
+ // add feed with no enclosures so that item ID != media ID
+ saveFeedlist(1, 10, false);
+
+ // add candidate for performAutoCleanup
+ List<Feed> feeds = saveFeedlist(1, 1, true);
+ FeedMedia m = feeds.get(0).getItems().get(0).getMedia();
+ //noinspection ConstantConditions
+ m.setDownloaded(true);
+ m.setLocalFileUrl("file");
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setMedia(m);
+ adapter.close();
+
+ testPerformAutoCleanupShouldNotDeleteBecauseInQueue();
+ }
+
+ @Test
+ public void testPerformAutoCleanupShouldNotDeleteBecauseFavorite() throws IOException {
+ final int numItems = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ populateItems(numItems, feed, items, files, FeedItem.PLAYED, false, true);
+
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (File file : files) {
+ assertTrue(file.exists());
+ }
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbNullCleanupAlgorithmTest.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbNullCleanupAlgorithmTest.java
new file mode 100644
index 000000000..032fc2013
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbNullCleanupAlgorithmTest.java
@@ -0,0 +1,125 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import androidx.preference.PreferenceManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager;
+import de.danoeh.antennapod.storage.database.DBWriter;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests that the APNullCleanupAlgorithm is working correctly.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class DbNullCleanupAlgorithmTest {
+
+ private static final int EPISODE_CACHE_SIZE = 5;
+
+ private Context context;
+
+ private File destFolder;
+
+ @Before
+ public void setUp() {
+ context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ destFolder = context.getExternalCacheDir();
+ cleanupDestFolder(destFolder);
+ assertNotNull(destFolder);
+ assertTrue(destFolder.exists());
+ assertTrue(destFolder.canWrite());
+
+ // create new database
+ PodDBAdapter.init(context);
+ PodDBAdapter.deleteDatabase();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.close();
+
+ SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(context
+ .getApplicationContext()).edit();
+ prefEdit.putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, Integer.toString(EPISODE_CACHE_SIZE));
+ prefEdit.putString(UserPreferences.PREF_EPISODE_CLEANUP,
+ Integer.toString(UserPreferences.EPISODE_CLEANUP_NULL));
+ prefEdit.commit();
+
+ UserPreferences.init(context);
+ AutoDownloadManager.setInstance(new AutoDownloadManagerImpl());
+ }
+
+ @After
+ public void tearDown() {
+ DBWriter.tearDownTests();
+ PodDBAdapter.deleteDatabase();
+ PodDBAdapter.tearDownTests();
+
+ cleanupDestFolder(destFolder);
+ assertTrue(destFolder.delete());
+ }
+
+ private void cleanupDestFolder(File destFolder) {
+ //noinspection ConstantConditions
+ for (File f : destFolder.listFiles()) {
+ assertTrue(f.delete());
+ }
+ }
+
+ /**
+ * A test with no items in the queue, but multiple items downloaded.
+ * The null algorithm should never delete any items, even if they're played and not in the queue.
+ */
+ @Test
+ public void testPerformAutoCleanupShouldNotDelete() throws IOException {
+ final int numItems = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ for (int i = 0; i < numItems; i++) {
+ FeedItem item = new FeedItem(0, "title", "id" + i, "link", new Date(), FeedItem.PLAYED, feed);
+
+ File f = new File(destFolder, "file " + i);
+ assertTrue(f.createNewFile());
+ files.add(f);
+ item.setMedia(new FeedMedia(0, item, 1, 0, 1L, "m", f.getAbsolutePath(), "url", true,
+ new Date(numItems - i), 0, 0));
+ items.add(item);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : items) {
+ assertTrue(item.getId() != 0);
+ //noinspection ConstantConditions
+ assertTrue(item.getMedia().getId() != 0);
+ }
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (int i = 0; i < files.size(); i++) {
+ assertTrue(files.get(i).exists());
+ }
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbQueueCleanupAlgorithmTest.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbQueueCleanupAlgorithmTest.java
new file mode 100644
index 000000000..b6d9a8f66
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbQueueCleanupAlgorithmTest.java
@@ -0,0 +1,54 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests that the APQueueCleanupAlgorithm is working correctly.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class DbQueueCleanupAlgorithmTest extends DbCleanupTests {
+
+ public DbQueueCleanupAlgorithmTest() {
+ setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_QUEUE);
+ AutoDownloadManager.setInstance(new AutoDownloadManagerImpl());
+ }
+
+ /**
+ * For APQueueCleanupAlgorithm we expect even unplayed episodes to be deleted if needed
+ * if they aren't in the queue.
+ */
+ @Test
+ public void testPerformAutoCleanupHandleUnplayed() throws IOException {
+ final int numItems = EPISODE_CACHE_SIZE * 2;
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ populateItems(numItems, feed, items, files, FeedItem.UNPLAYED, false, false);
+
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (int i = 0; i < files.size(); i++) {
+ if (i < EPISODE_CACHE_SIZE) {
+ assertTrue(files.get(i).exists());
+ } else {
+ assertFalse(files.get(i).exists());
+ }
+ }
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbReaderTest.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbReaderTest.java
new file mode 100644
index 000000000..f36408957
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbReaderTest.java
@@ -0,0 +1,526 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedCounter;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.model.feed.FeedOrder;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.database.DBWriter;
+import de.danoeh.antennapod.storage.database.NavDrawerData;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.storage.database.LongList;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.RobolectricTestRunner;
+
+import static de.danoeh.antennapod.net.download.service.episode.autodownload.DbTestUtils.saveFeedlist;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for DBReader.
+ */
+@SuppressWarnings("ConstantConditions")
+@RunWith(Enclosed.class)
+public class DbReaderTest {
+ @Ignore("Not a test")
+ public static class TestBase {
+ @Before
+ public void setUp() {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ UserPreferences.init(context);
+
+ PodDBAdapter.init(context);
+ PodDBAdapter.deleteDatabase();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.close();
+ }
+
+ @After
+ public void tearDown() {
+ PodDBAdapter.tearDownTests();
+ DBWriter.tearDownTests();
+ }
+ }
+
+ @RunWith(RobolectricTestRunner.class)
+ public static class SingleTests extends TestBase {
+ @Test
+ public void testGetFeedList() {
+ List<Feed> feeds = saveFeedlist(10, 0, false);
+ List<Feed> savedFeeds = DBReader.getFeedList();
+ assertNotNull(savedFeeds);
+ assertEquals(feeds.size(), savedFeeds.size());
+ for (int i = 0; i < feeds.size(); i++) {
+ assertEquals(feeds.get(i).getId(), savedFeeds.get(i).getId());
+ }
+ }
+
+ @Test
+ public void testGetFeedListSortOrder() {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+
+ final long lastRefreshed = System.currentTimeMillis();
+ Feed feed1 = new Feed(0, null, "A", "link", "d", null, null, null, "rss", "A", null, "", "", lastRefreshed);
+ Feed feed2 = new Feed(0, null, "b", "link", "d", null, null, null, "rss", "b", null, "", "", lastRefreshed);
+ Feed feed3 = new Feed(0, null, "C", "link", "d", null, null, null, "rss", "C", null, "", "", lastRefreshed);
+ Feed feed4 = new Feed(0, null, "d", "link", "d", null, null, null, "rss", "d", null, "", "", lastRefreshed);
+ adapter.setCompleteFeed(feed1);
+ adapter.setCompleteFeed(feed2);
+ adapter.setCompleteFeed(feed3);
+ adapter.setCompleteFeed(feed4);
+ assertTrue(feed1.getId() != 0);
+ assertTrue(feed2.getId() != 0);
+ assertTrue(feed3.getId() != 0);
+ assertTrue(feed4.getId() != 0);
+
+ adapter.close();
+
+ List<Feed> saved = DBReader.getFeedList();
+ assertNotNull(saved);
+ assertEquals("Wrong size: ", 4, saved.size());
+
+ assertEquals("Wrong id of feed 1: ", feed1.getId(), saved.get(0).getId());
+ assertEquals("Wrong id of feed 2: ", feed2.getId(), saved.get(1).getId());
+ assertEquals("Wrong id of feed 3: ", feed3.getId(), saved.get(2).getId());
+ assertEquals("Wrong id of feed 4: ", feed4.getId(), saved.get(3).getId());
+ }
+
+ @Test
+ public void testFeedListDownloadUrls() {
+ List<Feed> feeds = saveFeedlist(10, 0, false);
+ List<String> urls = DBReader.getFeedListDownloadUrls();
+ assertNotNull(urls);
+ assertEquals(feeds.size(), urls.size());
+ for (int i = 0; i < urls.size(); i++) {
+ assertEquals(urls.get(i), feeds.get(i).getDownloadUrl());
+ }
+ }
+
+ @Test
+ public void testLoadFeedDataOfFeedItemlist() {
+ final int numFeeds = 10;
+ final int numItems = 1;
+ List<Feed> feeds = saveFeedlist(numFeeds, numItems, false);
+ List<FeedItem> items = new ArrayList<>();
+ for (Feed f : feeds) {
+ for (FeedItem item : f.getItems()) {
+ item.setFeed(null);
+ item.setFeedId(f.getId());
+ items.add(item);
+ }
+ }
+ DBReader.loadAdditionalFeedItemListData(items);
+ for (int i = 0; i < numFeeds; i++) {
+ for (int j = 0; j < numItems; j++) {
+ FeedItem item = feeds.get(i).getItems().get(j);
+ assertNotNull(item.getFeed());
+ assertEquals(feeds.get(i).getId(), item.getFeed().getId());
+ assertEquals(item.getFeed().getId(), item.getFeedId());
+ }
+ }
+ }
+
+ @Test
+ public void testGetFeedItemList() {
+ final int numFeeds = 1;
+ final int numItems = 10;
+ Feed feed = saveFeedlist(numFeeds, numItems, false).get(0);
+ List<FeedItem> items = feed.getItems();
+ feed.setItems(null);
+ List<FeedItem> savedItems = DBReader.getFeedItemList(feed,
+ FeedItemFilter.unfiltered(), SortOrder.DATE_NEW_OLD);
+ assertNotNull(savedItems);
+ assertEquals(items.size(), savedItems.size());
+ for (int i = 0; i < savedItems.size(); i++) {
+ assertEquals(savedItems.get(i).getId(), items.get(i).getId());
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private List<FeedItem> saveQueue(int numItems) {
+ if (numItems <= 0) {
+ throw new IllegalArgumentException("numItems<=0");
+ }
+ List<Feed> feeds = saveFeedlist(numItems, numItems, false);
+ List<FeedItem> allItems = new ArrayList<>();
+ for (Feed f : feeds) {
+ allItems.addAll(f.getItems());
+ }
+ // take random items from every feed
+ Random random = new Random();
+ List<FeedItem> queue = new ArrayList<>();
+ while (queue.size() < numItems) {
+ int index = random.nextInt(numItems);
+ if (!queue.contains(allItems.get(index))) {
+ queue.add(allItems.get(index));
+ }
+ }
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setQueue(queue);
+ adapter.close();
+ return queue;
+ }
+
+ @Test
+ public void testGetQueueIdList() {
+ final int numItems = 10;
+ List<FeedItem> queue = saveQueue(numItems);
+ LongList ids = DBReader.getQueueIDList();
+ assertNotNull(ids);
+ assertEquals(ids.size(), queue.size());
+ for (int i = 0; i < queue.size(); i++) {
+ assertTrue(ids.get(i) != 0);
+ assertEquals(ids.get(i), queue.get(i).getId());
+ }
+ }
+
+ @Test
+ public void testGetQueue() {
+ final int numItems = 10;
+ List<FeedItem> queue = saveQueue(numItems);
+ List<FeedItem> savedQueue = DBReader.getQueue();
+ assertNotNull(savedQueue);
+ assertEquals(savedQueue.size(), queue.size());
+ for (int i = 0; i < queue.size(); i++) {
+ assertTrue(savedQueue.get(i).getId() != 0);
+ assertEquals(savedQueue.get(i).getId(), queue.get(i).getId());
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private List<FeedItem> saveDownloadedItems(int numItems) {
+ if (numItems <= 0) {
+ throw new IllegalArgumentException("numItems<=0");
+ }
+ List<Feed> feeds = saveFeedlist(numItems, numItems, true);
+ List<FeedItem> items = new ArrayList<>();
+ for (Feed f : feeds) {
+ items.addAll(f.getItems());
+ }
+ List<FeedItem> downloaded = new ArrayList<>();
+ Random random = new Random();
+
+ while (downloaded.size() < numItems) {
+ int i = random.nextInt(numItems);
+ if (!downloaded.contains(items.get(i))) {
+ FeedItem item = items.get(i);
+ item.getMedia().setDownloaded(true);
+ item.getMedia().setLocalFileUrl("file" + i);
+ downloaded.add(item);
+ }
+ }
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.storeFeedItemlist(downloaded);
+ adapter.close();
+ return downloaded;
+ }
+
+ @Test
+ public void testGetDownloadedItems() {
+ final int numItems = 10;
+ List<FeedItem> downloaded = saveDownloadedItems(numItems);
+ List<FeedItem> downloadedSaved = DBReader.getEpisodes(0, Integer.MAX_VALUE,
+ new FeedItemFilter(FeedItemFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD);
+ assertNotNull(downloadedSaved);
+ assertEquals(downloaded.size(), downloadedSaved.size());
+ for (FeedItem item : downloadedSaved) {
+ assertNotNull(item.getMedia());
+ assertTrue(item.getMedia().isDownloaded());
+ assertNotNull(item.getMedia().getDownloadUrl());
+ }
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private List<FeedItem> saveNewItems(int numItems) {
+ List<Feed> feeds = saveFeedlist(numItems, numItems, true);
+ List<FeedItem> items = new ArrayList<>();
+ for (Feed f : feeds) {
+ items.addAll(f.getItems());
+ }
+ List<FeedItem> newItems = new ArrayList<>();
+ Random random = new Random();
+
+ while (newItems.size() < numItems) {
+ int i = random.nextInt(numItems);
+ if (!newItems.contains(items.get(i))) {
+ FeedItem item = items.get(i);
+ item.setNew();
+ newItems.add(item);
+ }
+ }
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.storeFeedItemlist(newItems);
+ adapter.close();
+ return newItems;
+ }
+
+ @Test
+ public void testGetNewItemIds() {
+ final int numItems = 10;
+
+ List<FeedItem> newItems = saveNewItems(numItems);
+ long[] unreadIds = new long[newItems.size()];
+ for (int i = 0; i < newItems.size(); i++) {
+ unreadIds[i] = newItems.get(i).getId();
+ }
+ List<FeedItem> newItemsSaved = DBReader.getEpisodes(0, Integer.MAX_VALUE,
+ new FeedItemFilter(FeedItemFilter.NEW), SortOrder.DATE_NEW_OLD);
+ assertNotNull(newItemsSaved);
+ assertEquals(newItemsSaved.size(), newItems.size());
+ for (FeedItem feedItem : newItemsSaved) {
+ long savedId = feedItem.getId();
+ boolean found = false;
+ for (long id : unreadIds) {
+ if (id == savedId) {
+ found = true;
+ break;
+ }
+ }
+ assertTrue(found);
+ }
+ }
+
+ @Test
+ public void testGetPlaybackHistoryLength() {
+ final int totalItems = 100;
+
+ Feed feed = DbTestUtils.saveFeedlist(1, totalItems, true).get(0);
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ for (int playedItems : Arrays.asList(0, 1, 20, 100)) {
+ adapter.open();
+ for (int i = 0; i < playedItems; ++i) {
+ FeedMedia m = feed.getItems().get(i).getMedia();
+ m.setPlaybackCompletionDate(new Date(i + 1));
+
+ adapter.setFeedMediaPlaybackCompletionDate(m);
+ }
+ adapter.close();
+
+ long len = DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.IS_IN_HISTORY));
+ assertEquals("Wrong size: ", (int) len, playedItems);
+ }
+
+ }
+
+ @Test
+ public void testGetNavDrawerDataQueueEmptyNoUnreadItems() {
+ final int numFeeds = 10;
+ final int numItems = 10;
+ DbTestUtils.saveFeedlist(numFeeds, numItems, true);
+ NavDrawerData navDrawerData = DBReader.getNavDrawerData(
+ UserPreferences.getSubscriptionsFilter(), FeedOrder.COUNTER, FeedCounter.SHOW_NEW);
+ assertEquals(numFeeds, navDrawerData.items.size());
+ assertEquals(0, navDrawerData.numNewItems);
+ assertEquals(0, navDrawerData.queueSize);
+ }
+
+ @Test
+ public void testGetNavDrawerDataQueueNotEmptyWithUnreadItems() {
+ final int numFeeds = 10;
+ final int numItems = 10;
+ final int numQueue = 1;
+ final int numNew = 2;
+ List<Feed> feeds = DbTestUtils.saveFeedlist(numFeeds, numItems, true);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ for (int i = 0; i < numNew; i++) {
+ FeedItem item = feeds.get(0).getItems().get(i);
+ item.setNew();
+ adapter.setSingleFeedItem(item);
+ }
+ List<FeedItem> queue = new ArrayList<>();
+ for (int i = 0; i < numQueue; i++) {
+ FeedItem item = feeds.get(1).getItems().get(i);
+ queue.add(item);
+ }
+ adapter.setQueue(queue);
+
+ adapter.close();
+
+ NavDrawerData navDrawerData = DBReader.getNavDrawerData(
+ UserPreferences.getSubscriptionsFilter(), FeedOrder.COUNTER, FeedCounter.SHOW_NEW);
+ assertEquals(numFeeds, navDrawerData.items.size());
+ assertEquals(numNew, navDrawerData.numNewItems);
+ assertEquals(numQueue, navDrawerData.queueSize);
+ }
+
+ @Test
+ public void testGetFeedItemlistCheckChaptersFalse() {
+ List<Feed> feeds = DbTestUtils.saveFeedlist(10, 10, false, false, 0);
+ for (Feed feed : feeds) {
+ for (FeedItem item : feed.getItems()) {
+ assertFalse(item.hasChapters());
+ }
+ }
+ }
+
+ @Test
+ public void testGetFeedItemlistCheckChaptersTrue() {
+ List<Feed> feeds = saveFeedlist(10, 10, false, true, 10);
+ for (Feed feed : feeds) {
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.hasChapters());
+ }
+ }
+ }
+
+ @Test
+ public void testLoadChaptersOfFeedItemNoChapters() {
+ List<Feed> feeds = saveFeedlist(1, 3, false, false, 0);
+ saveFeedlist(1, 3, false, true, 3);
+ for (Feed feed : feeds) {
+ for (FeedItem item : feed.getItems()) {
+ assertFalse(item.hasChapters());
+ item.setChapters(DBReader.loadChaptersOfFeedItem(item));
+ assertFalse(item.hasChapters());
+ assertNull(item.getChapters());
+ }
+ }
+ }
+
+ @Test
+ public void testLoadChaptersOfFeedItemWithChapters() {
+ final int numChapters = 3;
+ DbTestUtils.saveFeedlist(1, 3, false, false, 0);
+ List<Feed> feeds = saveFeedlist(1, 3, false, true, numChapters);
+ for (Feed feed : feeds) {
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.hasChapters());
+ item.setChapters(DBReader.loadChaptersOfFeedItem(item));
+ assertTrue(item.hasChapters());
+ assertNotNull(item.getChapters());
+ assertEquals(numChapters, item.getChapters().size());
+ }
+ }
+ }
+
+ @Test
+ public void testGetItemWithChapters() {
+ final int numChapters = 3;
+ List<Feed> feeds = saveFeedlist(1, 1, false, true, numChapters);
+ FeedItem item1 = feeds.get(0).getItems().get(0);
+ FeedItem item2 = DBReader.getFeedItem(item1.getId());
+ item2.setChapters(DBReader.loadChaptersOfFeedItem(item2));
+ assertTrue(item2.hasChapters());
+ assertEquals(item1.getChapters().size(), item2.getChapters().size());
+ for (int i = 0; i < item1.getChapters().size(); i++) {
+ assertEquals(item1.getChapters().get(i).getId(), item2.getChapters().get(i).getId());
+ }
+ }
+
+ @Test
+ public void testGetItemByEpisodeUrl() {
+ List<Feed> feeds = saveFeedlist(1, 1, true);
+ FeedItem item1 = feeds.get(0).getItems().get(0);
+ FeedItem feedItemByEpisodeUrl = DBReader.getFeedItemByGuidOrEpisodeUrl(null,
+ item1.getMedia().getDownloadUrl());
+ assertEquals(item1.getItemIdentifier(), feedItemByEpisodeUrl.getItemIdentifier());
+ }
+
+ @Test
+ public void testGetItemByGuid() {
+ List<Feed> feeds = saveFeedlist(1, 1, true);
+ FeedItem item1 = feeds.get(0).getItems().get(0);
+
+ FeedItem feedItemByGuid = DBReader.getFeedItemByGuidOrEpisodeUrl(item1.getItemIdentifier(),
+ item1.getMedia().getDownloadUrl());
+ assertEquals(item1.getItemIdentifier(), feedItemByGuid.getItemIdentifier());
+ }
+
+ }
+
+ @RunWith(ParameterizedRobolectricTestRunner.class)
+ public static class PlaybackHistoryTest extends TestBase {
+
+ private int paramOffset;
+ private int paramLimit;
+
+ @ParameterizedRobolectricTestRunner.Parameters
+ public static Collection<Object[]> data() {
+ List<Integer> limits = Arrays.asList(1, 20, 100);
+ List<Integer> offsets = Arrays.asList(0, 10, 20);
+ Object[][] rv = new Object[limits.size() * offsets.size()][2];
+ int i = 0;
+ for (int offset : offsets) {
+ for (int limit : limits) {
+ rv[i][0] = offset;
+ rv[i][1] = limit;
+ i++;
+ }
+ }
+
+ return Arrays.asList(rv);
+ }
+
+ public PlaybackHistoryTest(int offset, int limit) {
+ this.paramOffset = offset;
+ this.paramLimit = limit;
+
+ }
+
+ @Test
+ public void testGetPlaybackHistory() {
+ final int numItems = (paramLimit + 1) * 2;
+ final int playedItems = paramLimit + 1;
+ final int numReturnedItems = Math.min(Math.max(playedItems - paramOffset, 0), paramLimit);
+ final int numFeeds = 1;
+
+ Feed feed = DbTestUtils.saveFeedlist(numFeeds, numItems, true).get(0);
+ long[] ids = new long[playedItems];
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ for (int i = 0; i < playedItems; i++) {
+ FeedMedia m = feed.getItems().get(i).getMedia();
+ m.setPlaybackCompletionDate(new Date(i + 1));
+ adapter.setFeedMediaPlaybackCompletionDate(m);
+ ids[ids.length - 1 - i] = m.getItem().getId();
+ }
+ adapter.close();
+
+ List<FeedItem> saved = DBReader.getEpisodes(paramOffset, paramLimit,
+ new FeedItemFilter(FeedItemFilter.IS_IN_HISTORY), SortOrder.COMPLETION_DATE_NEW_OLD);
+ assertNotNull(saved);
+ assertEquals(String.format("Wrong size with offset %d and limit %d: ",
+ paramOffset, paramLimit),
+ numReturnedItems, saved.size());
+ for (int i = 0; i < numReturnedItems; i++) {
+ FeedItem item = saved.get(i);
+ assertNotNull(item.getMedia().getPlaybackCompletionDate());
+ assertEquals(String.format("Wrong sort order with offset %d and limit %d: ",
+ paramOffset, paramLimit),
+ item.getId(), ids[paramOffset + i]);
+ }
+ }
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbTasksTest.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbTasksTest.java
new file mode 100644
index 000000000..776319acf
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbTasksTest.java
@@ -0,0 +1,249 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.database.DBWriter;
+import de.danoeh.antennapod.storage.database.FeedDatabaseWriter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+import static java.util.Collections.singletonList;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for {@link FeedDatabaseWriter}.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class DbTasksTest {
+ private Context context;
+
+ @Before
+ public void setUp() {
+ context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ UserPreferences.init(context);
+ PlaybackPreferences.init(context);
+
+ // create new database
+ PodDBAdapter.init(context);
+ PodDBAdapter.deleteDatabase();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.close();
+ }
+
+ @After
+ public void tearDown() {
+ DBWriter.tearDownTests();
+ PodDBAdapter.tearDownTests();
+ }
+
+ @Test
+ public void testUpdateFeedNewFeed() {
+ final int numItems = 10;
+
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < numItems; i++) {
+ feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i,
+ new Date(), FeedItem.UNPLAYED, feed));
+ }
+ Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
+
+ assertEquals(feed.getId(), newFeed.getId());
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertFalse(item.isPlayed());
+ assertTrue(item.getId() != 0);
+ }
+ }
+
+ /** Two feeds with the same title, but different download URLs should be treated as different feeds. */
+ @Test
+ public void testUpdateFeedSameTitle() {
+
+ Feed feed1 = new Feed("url1", null, "title");
+ Feed feed2 = new Feed("url2", null, "title");
+
+ feed1.setItems(new ArrayList<>());
+ feed2.setItems(new ArrayList<>());
+
+ Feed savedFeed1 = FeedDatabaseWriter.updateFeed(context, feed1, false);
+ Feed savedFeed2 = FeedDatabaseWriter.updateFeed(context, feed2, false);
+
+ assertTrue(savedFeed1.getId() != savedFeed2.getId());
+ }
+
+ @Test
+ public void testUpdateFeedUpdatedFeed() {
+ final int numItemsOld = 10;
+ final int numItemsNew = 10;
+
+ final Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < numItemsOld; i++) {
+ feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i,
+ new Date(i), FeedItem.PLAYED, feed));
+ }
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ // ensure that objects have been saved in db, then reset
+ assertTrue(feed.getId() != 0);
+ final long feedID = feed.getId();
+ feed.setId(0);
+ List<Long> itemIDs = new ArrayList<>();
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ itemIDs.add(item.getId());
+ item.setId(0);
+ }
+
+ for (int i = numItemsOld; i < numItemsNew + numItemsOld; i++) {
+ feed.getItems().add(0, new FeedItem(0, "item " + i, "id " + i, "link " + i,
+ new Date(i), FeedItem.UNPLAYED, feed));
+ }
+
+ final Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
+ assertNotSame(newFeed, feed);
+
+ updatedFeedTest(newFeed, feedID, itemIDs, numItemsOld, numItemsNew);
+
+ final Feed feedFromDB = DBReader.getFeed(newFeed.getId());
+ assertNotNull(feedFromDB);
+ assertEquals(newFeed.getId(), feedFromDB.getId());
+ updatedFeedTest(feedFromDB, feedID, itemIDs, numItemsOld, numItemsNew);
+ }
+
+ @Test
+ public void testUpdateFeedMediaUrlResetState() {
+ final Feed feed = new Feed("url", null, "title");
+ FeedItem item = new FeedItem(0, "item", "id", "link", new Date(), FeedItem.PLAYED, feed);
+ feed.setItems(singletonList(item));
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ // ensure that objects have been saved in db, then reset
+ assertTrue(feed.getId() != 0);
+ assertTrue(item.getId() != 0);
+
+ FeedMedia media = new FeedMedia(item, "url", 1024, "mime/type");
+ item.setMedia(media);
+ List<FeedItem> list = new ArrayList<>();
+ list.add(item);
+ feed.setItems(list);
+
+ final Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
+ assertNotSame(newFeed, feed);
+
+ final Feed feedFromDB = DBReader.getFeed(newFeed.getId());
+ final FeedItem feedItemFromDB = feedFromDB.getItems().get(0);
+ assertTrue(feedItemFromDB.isNew());
+ }
+
+ @Test
+ public void testUpdateFeedRemoveUnlistedItems() {
+ final Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < 10; i++) {
+ feed.getItems().add(
+ new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed));
+ }
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ // delete some items
+ feed.getItems().subList(0, 2).clear();
+ Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, true);
+ assertEquals(8, newFeed.getItems().size()); // 10 - 2 = 8 items
+
+ Feed feedFromDB = DBReader.getFeed(newFeed.getId());
+ assertEquals(8, feedFromDB.getItems().size()); // 10 - 2 = 8 items
+ }
+
+ @Test
+ public void testUpdateFeedSetDuplicate() {
+ final Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < 10; i++) {
+ FeedItem item =
+ new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), FeedItem.PLAYED, feed);
+ FeedMedia media = new FeedMedia(item, "download url " + i, 123, "media/mp3");
+ item.setMedia(media);
+ feed.getItems().add(item);
+ }
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ // change the guid of the first item, but leave the download url the same
+ FeedItem item = feed.getItemAtIndex(0);
+ item.setItemIdentifier("id 0-duplicate");
+ item.setTitle("item 0 duplicate");
+ Feed newFeed = FeedDatabaseWriter.updateFeed(context, feed, false);
+ assertEquals(10, newFeed.getItems().size()); // id 1-duplicate replaces because the stream url is the same
+
+ Feed feedFromDB = DBReader.getFeed(newFeed.getId());
+ assertEquals(10, feedFromDB.getItems().size()); // id1-duplicate should override id 1
+
+ FeedItem updatedItem = feedFromDB.getItemAtIndex(9);
+ assertEquals("item 0 duplicate", updatedItem.getTitle());
+ assertEquals("id 0-duplicate", updatedItem.getItemIdentifier()); // Should use the new ID for sync etc
+ }
+
+
+ @SuppressWarnings("SameParameterValue")
+ private void updatedFeedTest(final Feed newFeed, long feedID, List<Long> itemIDs,
+ int numItemsOld, int numItemsNew) {
+ assertEquals(feedID, newFeed.getId());
+ assertEquals(numItemsNew + numItemsOld, newFeed.getItems().size());
+ Collections.reverse(newFeed.getItems());
+ Date lastDate = new Date(0);
+ for (int i = 0; i < numItemsOld; i++) {
+ FeedItem item = newFeed.getItems().get(i);
+ assertSame(newFeed, item.getFeed());
+ assertEquals((long) itemIDs.get(i), item.getId());
+ assertTrue(item.isPlayed());
+ assertTrue(item.getPubDate().getTime() >= lastDate.getTime());
+ lastDate = item.getPubDate();
+ }
+ for (int i = numItemsOld; i < numItemsNew + numItemsOld; i++) {
+ FeedItem item = newFeed.getItems().get(i);
+ assertSame(newFeed, item.getFeed());
+ assertTrue(item.getId() != 0);
+ assertFalse(item.isPlayed());
+ assertTrue(item.getPubDate().getTime() >= lastDate.getTime());
+ lastDate = item.getPubDate();
+ }
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbTestUtils.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbTestUtils.java
new file mode 100644
index 000000000..c104df9e8
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbTestUtils.java
@@ -0,0 +1,74 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import de.danoeh.antennapod.model.feed.Chapter;
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Utility methods for DB* tests.
+ */
+abstract class DbTestUtils {
+
+ /**
+ * Use this method when tests don't involve chapters.
+ */
+ public static List<Feed> saveFeedlist(int numFeeds, int numItems, boolean withMedia) {
+ return saveFeedlist(numFeeds, numItems, withMedia, false, 0);
+ }
+
+ /**
+ * Use this method when tests involve chapters.
+ */
+ public static List<Feed> saveFeedlist(int numFeeds, int numItems, boolean withMedia,
+ boolean withChapters, int numChapters) {
+ if (numFeeds <= 0) {
+ throw new IllegalArgumentException("numFeeds<=0");
+ }
+ if (numItems < 0) {
+ throw new IllegalArgumentException("numItems<0");
+ }
+
+ List<Feed> feeds = new ArrayList<>();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ for (int i = 0; i < numFeeds; i++) {
+ Feed f = new Feed(0, null, "feed " + i, "link" + i, "descr", null, null,
+ null, null, "id" + i, null, null, "url" + i, System.currentTimeMillis());
+ f.setItems(new ArrayList<>());
+ for (int j = 0; j < numItems; j++) {
+ FeedItem item = new FeedItem(0, "item " + j, "id" + j, "link" + j, new Date(),
+ FeedItem.PLAYED, f, withChapters);
+ if (withMedia) {
+ FeedMedia media = new FeedMedia(item, "url" + j, 1, "audio/mp3");
+ item.setMedia(media);
+ }
+ if (withChapters) {
+ List<Chapter> chapters = new ArrayList<>();
+ item.setChapters(chapters);
+ for (int k = 0; k < numChapters; k++) {
+ chapters.add(new Chapter(k, "item " + j + " chapter " + k,
+ "http://example.com", "http://example.com/image.png"));
+ }
+ }
+ f.getItems().add(item);
+ }
+ adapter.setCompleteFeed(f);
+ assertTrue(f.getId() != 0);
+ for (FeedItem item : f.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ feeds.add(f);
+ }
+ adapter.close();
+
+ return feeds;
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbWriterTest.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbWriterTest.java
new file mode 100644
index 000000000..38d3e5dd0
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/DbWriterTest.java
@@ -0,0 +1,832 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.util.Log;
+
+import androidx.core.util.Consumer;
+import androidx.preference.PreferenceManager;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import de.danoeh.antennapod.model.feed.FeedItemFilter;
+import de.danoeh.antennapod.model.feed.SortOrder;
+import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface;
+import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfaceStub;
+import de.danoeh.antennapod.storage.database.DBReader;
+import de.danoeh.antennapod.storage.database.DBWriter;
+import de.danoeh.antennapod.storage.database.PodDBAdapter;
+import org.awaitility.Awaitility;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.model.feed.FeedMedia;
+import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+import de.danoeh.antennapod.core.util.FeedItemUtil;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test class for {@link DBWriter}.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class DbWriterTest {
+
+ private static final String TAG = "DBWriterTest";
+ private static final String TEST_FOLDER = "testDBWriter";
+ private static final long TIMEOUT = 5L;
+
+ private Context context;
+
+ @Before
+ public void setUp() {
+ context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ UserPreferences.init(context);
+ PlaybackPreferences.init(context);
+ DownloadServiceInterface.setImpl(new DownloadServiceInterfaceStub());
+
+ // create new database
+ PodDBAdapter.init(context);
+ PodDBAdapter.deleteDatabase();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.close();
+
+ SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(
+ context.getApplicationContext()).edit();
+ prefEdit.putBoolean(UserPreferences.PREF_DELETE_REMOVES_FROM_QUEUE, true).commit();
+ }
+
+ @After
+ public void tearDown() {
+ PodDBAdapter.tearDownTests();
+ DBWriter.tearDownTests();
+
+ File testDir = context.getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(testDir);
+ for (File f : testDir.listFiles()) {
+ //noinspection ResultOfMethodCallIgnored
+ f.delete();
+ }
+ }
+
+ @Test
+ public void testSetFeedMediaPlaybackInformation() throws Exception {
+ final int position = 50;
+ final long lastPlayedTime = 1000;
+ final int playedDuration = 60;
+ final int duration = 100;
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.PLAYED, feed);
+ items.add(item);
+ FeedMedia media = new FeedMedia(0, item, duration, 1, 1, "mime_type",
+ "dummy path", "download_url", true, null, 0, 0);
+ item.setMedia(media);
+
+ DBWriter.setFeedItem(item).get(TIMEOUT, TimeUnit.SECONDS);
+
+ media.setPosition(position);
+ media.setLastPlayedTime(lastPlayedTime);
+ media.setPlayedDuration(playedDuration);
+
+ DBWriter.setFeedMediaPlaybackInformation(item.getMedia()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ FeedItem itemFromDb = DBReader.getFeedItem(item.getId());
+ FeedMedia mediaFromDb = itemFromDb.getMedia();
+
+ assertEquals(position, mediaFromDb.getPosition());
+ assertEquals(lastPlayedTime, mediaFromDb.getLastPlayedTime());
+ assertEquals(playedDuration, mediaFromDb.getPlayedDuration());
+ assertEquals(duration, mediaFromDb.getDuration());
+ }
+
+ @Test
+ public void testDeleteFeedMediaOfItemFileExists() throws Exception {
+ File dest = new File(context.getExternalFilesDir(TEST_FOLDER), "testFile");
+
+ assertTrue(dest.createNewFile());
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.PLAYED, feed);
+
+ FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type",
+ dest.getAbsolutePath(), "download_url", true, null, 0, 0);
+ item.setMedia(media);
+
+ items.add(item);
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+ assertTrue(media.getId() != 0);
+ assertTrue(item.getId() != 0);
+
+ DBWriter.deleteFeedMediaOfItem(context, media)
+ .get(TIMEOUT, TimeUnit.SECONDS);
+ media = DBReader.getFeedMedia(media.getId());
+ assertNotNull(media);
+ assertFalse(dest.exists());
+ assertFalse(media.isDownloaded());
+ assertNull(media.getLocalFileUrl());
+ }
+
+ @Test
+ public void testDeleteFeedMediaOfItemRemoveFromQueue() throws Exception {
+ assertTrue(UserPreferences.shouldDeleteRemoveFromQueue());
+
+ File dest = new File(context.getExternalFilesDir(TEST_FOLDER), "testFile");
+
+ assertTrue(dest.createNewFile());
+
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ FeedItem item = new FeedItem(0, "Item", "Item", "url", new Date(), FeedItem.UNPLAYED, feed);
+
+ FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type",
+ dest.getAbsolutePath(), "download_url", true, null, 0, 0);
+ item.setMedia(media);
+
+ items.add(item);
+ List<FeedItem> queue = new ArrayList<>();
+ queue.add(item);
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.setQueue(queue);
+ adapter.close();
+ assertTrue(media.getId() != 0);
+ assertTrue(item.getId() != 0);
+ queue = DBReader.getQueue();
+ assertFalse(queue.isEmpty());
+
+ DBWriter.deleteFeedMediaOfItem(context, media);
+ Awaitility.await().timeout(2, TimeUnit.SECONDS).until(() -> !dest.exists());
+ media = DBReader.getFeedMedia(media.getId());
+ assertNotNull(media);
+ assertFalse(dest.exists());
+ assertFalse(media.isDownloaded());
+ assertNull(media.getLocalFileUrl());
+ Awaitility.await().timeout(2, TimeUnit.SECONDS).until(() -> DBReader.getQueue().isEmpty());
+ }
+
+ @Test
+ public void testDeleteFeed() throws Exception {
+ File destFolder = context.getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+
+ List<File> itemFiles = new ArrayList<>();
+ // create items with downloaded media files
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed);
+ feed.getItems().add(item);
+
+ File enc = new File(destFolder, "file " + i);
+ assertTrue(enc.createNewFile());
+
+ itemFiles.add(enc);
+ FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type",
+ enc.getAbsolutePath(), "download_url", true, null, 0, 0);
+ item.setMedia(media);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+
+ DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ // check if files still exist
+ for (File f : itemFiles) {
+ assertFalse(f.exists());
+ }
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertEquals(0, c.getCount());
+ c.close();
+ c = adapter.getFeedItemFromMediaIdCursor(item.getMedia().getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+ adapter.close();
+ }
+
+ @Test
+ public void testDeleteFeedNoItems() throws Exception {
+ File destFolder = context.getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(null);
+ feed.setImageUrl("url");
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+
+ DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ adapter.close();
+ }
+
+ @Test
+ public void testDeleteFeedNoFeedMedia() throws Exception {
+ File destFolder = context.getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+
+ feed.setImageUrl("url");
+
+ // create items
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed);
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+
+ DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+ adapter.close();
+ }
+
+ @Test
+ public void testDeleteFeedWithQueueItems() throws Exception {
+ File destFolder = context.getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+
+ feed.setImageUrl("url");
+
+ // create items with downloaded media files
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed);
+ feed.getItems().add(item);
+ File enc = new File(destFolder, "file " + i);
+ FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type",
+ enc.getAbsolutePath(), "download_url", false, null, 0, 0);
+ item.setMedia(media);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+
+ List<FeedItem> queue = new ArrayList<>(feed.getItems());
+ adapter.open();
+ adapter.setQueue(queue);
+
+ Cursor queueCursor = adapter.getQueueIDCursor();
+ assertEquals(queue.size(), queueCursor.getCount());
+ queueCursor.close();
+
+ adapter.close();
+ DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+ adapter.open();
+
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertEquals(0, c.getCount());
+ c.close();
+ c = adapter.getFeedItemFromMediaIdCursor(item.getMedia().getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+ c = adapter.getQueueCursor();
+ assertEquals(0, c.getCount());
+ c.close();
+ adapter.close();
+ }
+
+ @Test
+ public void testDeleteFeedNoDownloadedFiles() throws Exception {
+ File destFolder = context.getExternalFilesDir(TEST_FOLDER);
+ assertNotNull(destFolder);
+
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+
+ feed.setImageUrl("url");
+
+ // create items with downloaded media files
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed);
+ feed.getItems().add(item);
+ File enc = new File(destFolder, "file " + i);
+ FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type",
+ enc.getAbsolutePath(), "download_url", false, null, 0, 0);
+ item.setMedia(media);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ assertTrue(item.getMedia().getId() != 0);
+ }
+
+ DBWriter.deleteFeed(context, feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor c = adapter.getFeedCursor(feed.getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ for (FeedItem item : feed.getItems()) {
+ c = adapter.getFeedItemCursor(String.valueOf(item.getId()));
+ assertEquals(0, c.getCount());
+ c.close();
+ c = adapter.getFeedItemFromMediaIdCursor(item.getMedia().getId());
+ assertEquals(0, c.getCount());
+ c.close();
+ }
+ adapter.close();
+ }
+
+ @Test
+ public void testDeleteFeedItems() throws Exception {
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ feed.setImageUrl("url");
+
+ // create items
+ for (int i = 0; i < 10; i++) {
+ FeedItem item = new FeedItem(0, "Item " + i, "Item" + i, "url", new Date(), FeedItem.PLAYED, feed);
+ item.setMedia(new FeedMedia(item, "", 0, ""));
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ List<FeedItem> itemsToDelete = feed.getItems().subList(0, 2);
+ DBWriter.deleteFeedItems(context, itemsToDelete).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ for (int i = 0; i < feed.getItems().size(); i++) {
+ FeedItem feedItem = feed.getItems().get(i);
+ Cursor c = adapter.getFeedItemCursor(String.valueOf(feedItem.getId()));
+ if (i < 2) {
+ assertEquals(0, c.getCount());
+ } else {
+ assertEquals(1, c.getCount());
+ }
+ c.close();
+ }
+ adapter.close();
+ }
+
+ private FeedMedia playbackHistorySetup(Date playbackCompletionDate) {
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed);
+ FeedMedia media = new FeedMedia(0, item, 10, 0, 1, "mime", null,
+ "url", false, playbackCompletionDate, 0, 0);
+ feed.getItems().add(item);
+ item.setMedia(media);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+ assertTrue(media.getId() != 0);
+ return media;
+ }
+
+ @Test
+ public void testAddItemToPlaybackHistoryNotPlayedYet() throws Exception {
+ FeedMedia media = playbackHistorySetup(null);
+ DBWriter.addItemToPlaybackHistory(media).get(TIMEOUT, TimeUnit.SECONDS);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ media = DBReader.getFeedMedia(media.getId());
+ adapter.close();
+
+ assertNotNull(media);
+ assertNotNull(media.getPlaybackCompletionDate());
+ }
+
+ @Test
+ public void testAddItemToPlaybackHistoryAlreadyPlayed() throws Exception {
+ final long oldDate = 0;
+
+ FeedMedia media = playbackHistorySetup(new Date(oldDate));
+ DBWriter.addItemToPlaybackHistory(media).get(TIMEOUT, TimeUnit.SECONDS);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ media = DBReader.getFeedMedia(media.getId());
+ adapter.close();
+
+ assertNotNull(media);
+ assertNotNull(media.getPlaybackCompletionDate());
+ assertNotEquals(media.getPlaybackCompletionDate().getTime(), oldDate);
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private Feed queueTestSetupMultipleItems(final int numItems) throws Exception {
+ UserPreferences.setEnqueueLocation(UserPreferences.EnqueueLocation.BACK);
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < numItems; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed);
+ item.setMedia(new FeedMedia(item, "", 0, ""));
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ List<Future<?>> futures = new ArrayList<>();
+ for (FeedItem item : feed.getItems()) {
+ futures.add(DBWriter.addQueueItem(context, item));
+ }
+ for (Future<?> f : futures) {
+ f.get(TIMEOUT, TimeUnit.SECONDS);
+ }
+ return feed;
+ }
+
+ @Test
+ public void testAddQueueItemSingleItem() throws Exception {
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed);
+ item.setMedia(new FeedMedia(item, "", 0, ""));
+ feed.getItems().add(item);
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(item.getId() != 0);
+ DBWriter.addQueueItem(context, item).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor cursor = adapter.getQueueIDCursor();
+ assertTrue(cursor.moveToFirst());
+ assertEquals(item.getId(), cursor.getLong(0));
+ cursor.close();
+ adapter.close();
+ }
+
+ @Test
+ public void testAddQueueItemSingleItemAlreadyInQueue() throws Exception {
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ FeedItem item = new FeedItem(0, "title", "id", "link", new Date(), FeedItem.PLAYED, feed);
+ item.setMedia(new FeedMedia(item, "", 0, ""));
+ feed.getItems().add(item);
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(item.getId() != 0);
+ DBWriter.addQueueItem(context, item).get(TIMEOUT, TimeUnit.SECONDS);
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor cursor = adapter.getQueueIDCursor();
+ assertTrue(cursor.moveToFirst());
+ assertEquals(item.getId(), cursor.getLong(0));
+ cursor.close();
+ adapter.close();
+
+ DBWriter.addQueueItem(context, item).get(TIMEOUT, TimeUnit.SECONDS);
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ cursor = adapter.getQueueIDCursor();
+ assertTrue(cursor.moveToFirst());
+ assertEquals(item.getId(), cursor.getLong(0));
+ assertEquals(1, cursor.getCount());
+ cursor.close();
+ adapter.close();
+ }
+
+ @Test
+ public void testAddQueueItemMultipleItems() throws Exception {
+ final int numItems = 10;
+
+ Feed feed;
+ feed = queueTestSetupMultipleItems(numItems);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor cursor = adapter.getQueueIDCursor();
+ assertTrue(cursor.moveToFirst());
+ assertEquals(numItems, cursor.getCount());
+ List<Long> expectedIds;
+ expectedIds = FeedItemUtil.getIdList(feed.getItems());
+ List<Long> actualIds = new ArrayList<>();
+ for (int i = 0; i < numItems; i++) {
+ assertTrue(cursor.moveToPosition(i));
+ actualIds.add(cursor.getLong(0));
+ }
+ cursor.close();
+ adapter.close();
+ assertEquals("Bulk add to queue: result order should be the same as the order given",
+ expectedIds, actualIds);
+ }
+
+ @Test
+ public void testClearQueue() throws Exception {
+ final int numItems = 10;
+
+ queueTestSetupMultipleItems(numItems);
+ DBWriter.clearQueue().get(TIMEOUT, TimeUnit.SECONDS);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor cursor = adapter.getQueueIDCursor();
+ assertFalse(cursor.moveToFirst());
+ cursor.close();
+ adapter.close();
+ }
+
+ @Test
+ public void testRemoveQueueItem() throws Exception {
+ final int numItems = 10;
+ Feed feed = createTestFeed(numItems);
+
+ for (int removeIndex = 0; removeIndex < numItems; removeIndex++) {
+ final FeedItem item = feed.getItems().get(removeIndex);
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setQueue(feed.getItems());
+ adapter.close();
+
+ DBWriter.removeQueueItem(context, false, item).get(TIMEOUT, TimeUnit.SECONDS);
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor queue = adapter.getQueueIDCursor();
+ assertEquals(numItems - 1, queue.getCount());
+ for (int i = 0; i < queue.getCount(); i++) {
+ assertTrue(queue.moveToPosition(i));
+ final long queueID = queue.getLong(0);
+ assertTrue(queueID != item.getId()); // removed item is no longer in queue
+ boolean idFound = false;
+ for (FeedItem other : feed.getItems()) { // items that were not removed are still in the queue
+ idFound = idFound | (other.getId() == queueID);
+ }
+ assertTrue(idFound);
+ }
+ queue.close();
+ adapter.close();
+ }
+ }
+
+ @Test
+ public void testRemoveQueueItemMultipleItems() throws Exception {
+ final int numItems = 5;
+ final int numInQueue = numItems - 1; // the last one not in queue for boundary condition
+ Feed feed = createTestFeed(numItems);
+
+ List<FeedItem> itemsToAdd = feed.getItems().subList(0, numInQueue);
+ withPodDB(adapter -> adapter.setQueue(itemsToAdd));
+
+ // Actual tests
+ //
+
+ // Use array rather than List to make codes more succinct
+ Long[] itemIds = toItemIds(feed.getItems()).toArray(new Long[0]);
+
+ DBWriter.removeQueueItem(context, false,
+ itemIds[1], itemIds[3]).get(TIMEOUT, TimeUnit.SECONDS);
+ assertQueueByItemIds("Average case - 2 items removed successfully",
+ itemIds[0], itemIds[2]);
+
+ DBWriter.removeQueueItem(context, false).get(TIMEOUT, TimeUnit.SECONDS);
+ assertQueueByItemIds("Boundary case - no items supplied. queue should see no change",
+ itemIds[0], itemIds[2]);
+
+ DBWriter.removeQueueItem(context, false,
+ itemIds[0], itemIds[4], -1L).get(TIMEOUT, TimeUnit.SECONDS);
+ assertQueueByItemIds("Boundary case - items not in queue ignored",
+ itemIds[2]);
+
+ DBWriter.removeQueueItem(context, false,
+ itemIds[2], -1L).get(TIMEOUT, TimeUnit.SECONDS);
+ assertQueueByItemIds("Boundary case - invalid itemIds ignored"); // the queue is empty
+
+ }
+
+ @Test
+ public void testMoveQueueItem() throws Exception {
+ final int numItems = 10;
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < numItems; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i,
+ new Date(), FeedItem.PLAYED, feed);
+ item.setMedia(new FeedMedia(item, "", 0, ""));
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ for (int from = 0; from < numItems; from++) {
+ for (int to = 0; to < numItems; to++) {
+ if (from == to) {
+ continue;
+ }
+ Log.d(TAG, String.format(Locale.US, "testMoveQueueItem: From=%d, To=%d", from, to));
+ final long fromID = feed.getItems().get(from).getId();
+
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setQueue(feed.getItems());
+ adapter.close();
+
+ DBWriter.moveQueueItem(from, to, false).get(TIMEOUT, TimeUnit.SECONDS);
+ adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ Cursor queue = adapter.getQueueIDCursor();
+ assertEquals(numItems, queue.getCount());
+ assertTrue(queue.moveToPosition(from));
+ assertNotEquals(fromID, queue.getLong(0));
+ assertTrue(queue.moveToPosition(to));
+ assertEquals(fromID, queue.getLong(0));
+
+ queue.close();
+ adapter.close();
+ }
+ }
+ }
+
+ @Test
+ public void testRemoveAllNewFlags() throws Exception {
+ final int numItems = 10;
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < numItems; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i,
+ new Date(), FeedItem.NEW, feed);
+ item.setMedia(new FeedMedia(item, "", 0, ""));
+ feed.getItems().add(item);
+ }
+
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+
+ assertTrue(feed.getId() != 0);
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+
+ DBWriter.removeAllNewFlags().get();
+ List<FeedItem> loadedItems = DBReader.getFeedItemList(feed,
+ FeedItemFilter.unfiltered(), SortOrder.DATE_NEW_OLD);
+ for (FeedItem item : loadedItems) {
+ assertFalse(item.isNew());
+ }
+ }
+
+ private static Feed createTestFeed(int numItems) {
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < numItems; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i,
+ new Date(), FeedItem.PLAYED, feed);
+ item.setMedia(new FeedMedia(item, "", 0, ""));
+ feed.getItems().add(item);
+ }
+
+ withPodDB(adapter -> adapter.setCompleteFeed(feed));
+
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ return feed;
+ }
+
+ private static void withPodDB(Consumer<PodDBAdapter> action) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ try {
+ adapter.open();
+ action.accept(adapter);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ private static void assertQueueByItemIds(String message, long... itemIdsExpected) {
+ List<FeedItem> queue = DBReader.getQueue();
+ List<Long> itemIdsActualList = toItemIds(queue);
+ List<Long> itemIdsExpectedList = new ArrayList<>(itemIdsExpected.length);
+ for (long id : itemIdsExpected) {
+ itemIdsExpectedList.add(id);
+ }
+
+ assertEquals(message, itemIdsExpectedList, itemIdsActualList);
+ }
+
+ private static List<Long> toItemIds(List<FeedItem> items) {
+ List<Long> itemIds = new ArrayList<>(items.size());
+ for (FeedItem item : items) {
+ itemIds.add(item.getId());
+ }
+ return itemIds;
+ }
+}
diff --git a/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/ExceptFavoriteCleanupAlgorithmTest.java b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/ExceptFavoriteCleanupAlgorithmTest.java
new file mode 100644
index 000000000..dd77606dc
--- /dev/null
+++ b/net/download/service/src/test/java/de/danoeh/antennapod/net/download/service/episode/autodownload/ExceptFavoriteCleanupAlgorithmTest.java
@@ -0,0 +1,91 @@
+package de.danoeh.antennapod.net.download.service.episode.autodownload;
+
+import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import de.danoeh.antennapod.model.feed.Feed;
+import de.danoeh.antennapod.model.feed.FeedItem;
+import de.danoeh.antennapod.storage.preferences.UserPreferences;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests that the APFavoriteCleanupAlgorithm is working correctly.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class ExceptFavoriteCleanupAlgorithmTest extends DbCleanupTests {
+ private final int numberOfItems = EPISODE_CACHE_SIZE * 2;
+
+ public ExceptFavoriteCleanupAlgorithmTest() {
+ setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE);
+ AutoDownloadManager.setInstance(new AutoDownloadManagerImpl());
+ }
+
+ @Test
+ public void testPerformAutoCleanupHandleUnplayed() throws IOException {
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, false, false);
+
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (int i = 0; i < files.size(); i++) {
+ if (i < EPISODE_CACHE_SIZE) {
+ assertTrue("Only enough items should be deleted", files.get(i).exists());
+ } else {
+ assertFalse("Expected episode to be deleted", files.get(i).exists());
+ }
+ }
+ }
+
+ @Test
+ public void testPerformAutoCleanupDeletesQueued() throws IOException {
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, true, false);
+
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (int i = 0; i < files.size(); i++) {
+ if (i < EPISODE_CACHE_SIZE) {
+ assertTrue("Only enough items should be deleted", files.get(i).exists());
+ } else {
+ assertFalse("Queued episodes should be deleted", files.get(i).exists());
+ }
+ }
+ }
+
+ @Test
+ public void testPerformAutoCleanupSavesFavorited() throws IOException {
+ Feed feed = new Feed("url", null, "title");
+ List<FeedItem> items = new ArrayList<>();
+ feed.setItems(items);
+ List<File> files = new ArrayList<>();
+ populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, false, true);
+
+ AutoDownloadManager.getInstance().performAutoCleanup(context);
+ for (int i = 0; i < files.size(); i++) {
+ assertTrue("Favorite episodes should should not be deleted", files.get(i).exists());
+ }
+ }
+
+ @Override
+ public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue() throws IOException {
+ // Yes it should
+ }
+
+ @Override
+ public void testPerformAutoCleanupShouldNotDeleteBecauseInQueue_withFeedsWithNoMedia() throws IOException {
+ // Yes it should
+ }
+}