diff --git a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java index 3cdb09605..bba546a88 100644 --- a/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java +++ b/app/src/androidTest/java/de/test/antennapod/ui/PreferencesTest.java @@ -15,6 +15,7 @@ import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.QueueFragment; import de.danoeh.antennapod.fragment.SubscriptionFragment; @@ -371,6 +372,17 @@ public class PreferencesTest { .until(() -> enableAutodownloadOnBattery == UserPreferences.isEnableAutodownloadOnBattery()); } + @Test + public void testEpisodeCleanupFavoriteOnly() { + clickPreference(R.string.network_pref); + onView(withText(R.string.pref_automatic_download_title)).perform(click()); + onView(withText(R.string.pref_episode_cleanup_title)).perform(click()); + onView(isRoot()).perform(waitForView(withText(R.string.episode_cleanup_except_favorite_removal), 1000)); + onView(withText(R.string.episode_cleanup_except_favorite_removal)).perform(click()); + Awaitility.await().atMost(1000, MILLISECONDS) + .until(() -> UserPreferences.getEpisodeCleanupAlgorithm() instanceof ExceptFavoriteCleanupAlgorithm); + } + @Test public void testEpisodeCleanupQueueOnly() { clickPreference(R.string.network_pref); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java index 0d6e79e84..ec61c82f2 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/AutoDownloadPreferencesFragment.java @@ -174,7 +174,9 @@ public class AutoDownloadPreferencesFragment extends PreferenceFragmentCompat { String[] entries = new String[values.length]; for (int x = 0; x < values.length; x++) { int v = Integer.parseInt(values[x]); - if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { + if (v == UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE) { + entries[x] = res.getString(R.string.episode_cleanup_except_favorite_removal); + } else if (v == UserPreferences.EPISODE_CLEANUP_QUEUE) { entries[x] = res.getString(R.string.episode_cleanup_queue_removal); } else if (v == UserPreferences.EPISODE_CLEANUP_NULL){ entries[x] = res.getString(R.string.episode_cleanup_never); diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index 3d8d1b6bc..807348896 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -36,6 +36,7 @@ import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.service.download.ProxyConfig; import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.ExceptFavoriteCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; @@ -137,6 +138,7 @@ public class UserPreferences { public static final String PREF_CAST_ENABLED = "prefCast"; //Used for enabling Chromecast support public static final int EPISODE_CLEANUP_QUEUE = -1; public static final int EPISODE_CLEANUP_NULL = -2; + public static final int EPISODE_CLEANUP_EXCEPT_FAVORITE = -3; public static final int EPISODE_CLEANUP_DEFAULT = 0; // Constants @@ -882,7 +884,9 @@ public class UserPreferences { return new APNullCleanupAlgorithm(); } int cleanupValue = getEpisodeCleanupValue(); - if (cleanupValue == EPISODE_CLEANUP_QUEUE) { + if (cleanupValue == EPISODE_CLEANUP_EXCEPT_FAVORITE) { + return new ExceptFavoriteCleanupAlgorithm(); + } else if (cleanupValue == EPISODE_CLEANUP_QUEUE) { return new APQueueCleanupAlgorithm(); } else if (cleanupValue == EPISODE_CLEANUP_NULL) { return new APNullCleanupAlgorithm(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java new file mode 100644 index 000000000..f0788db33 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java @@ -0,0 +1,99 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ExecutionException; + +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +/** + * A cleanup algorithm that removes any item that isn't a favorite but only if space is needed. + */ +public class ExceptFavoriteCleanupAlgorithm extends EpisodeCleanupAlgorithm { + + private static final String TAG = "ExceptFavCleanupAlgo"; + + /** + * The maximum number of episodes that could be cleaned up. + * + * @return the number of episodes that *could* be cleaned up, if needed + */ + public int getReclaimableItems() { + return getCandidates().size(); + } + + @Override + public int performCleanup(Context context, int numberOfEpisodesToDelete) { + List candidates = getCandidates(); + List delete; + + // in the absence of better data, we'll sort by item publication date + Collections.sort(candidates, (lhs, rhs) -> { + Date l = lhs.getPubDate(); + Date r = rhs.getPubDate(); + + if (l != null && r != null) { + return l.compareTo(r); + } else { + // No date - compare by id which should be always incremented + return Long.compare(lhs.getId(), rhs.getId()); + } + }); + + if (candidates.size() > numberOfEpisodesToDelete) { + delete = candidates.subList(0, numberOfEpisodesToDelete); + } else { + delete = candidates; + } + + for (FeedItem item : delete) { + try { + DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + int counter = delete.size(); + Log.i(TAG, String.format(Locale.US, + "Auto-delete deleted %d episodes (%d requested)", counter, + numberOfEpisodesToDelete)); + + return counter; + } + + @NonNull + private List getCandidates() { + List candidates = new ArrayList<>(); + List downloadedItems = DBReader.getDownloadedItems(); + for (FeedItem item : downloadedItems) { + if (item.hasMedia() + && item.getMedia().isDownloaded() + && !item.isTagged(FeedItem.TAG_FAVORITE)) { + candidates.add(item); + } + } + return candidates; + } + + @Override + public int getDefaultCleanupParameter() { + int cacheSize = UserPreferences.getEpisodeCacheSize(); + if (cacheSize != UserPreferences.getEpisodeCacheSizeUnlimited()) { + int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(); + if (downloadedEpisodes > cacheSize) { + return downloadedEpisodes - cacheSize; + } + } + return 0; + } +} diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml index 6492c1aa2..6b3a10f46 100644 --- a/core/src/main/res/values/arrays.xml +++ b/core/src/main/res/values/arrays.xml @@ -96,6 +96,7 @@ + @string/episode_cleanup_except_favorite_removal @string/episode_cleanup_queue_removal 0 1 @@ -133,6 +134,7 @@ + -3 -1 0 12 diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 0cd8bcd54..91ec75782 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -116,6 +116,7 @@ Never Send… Never + When not favorited When not in queue After finishing @@ -387,7 +388,7 @@ Clear history Media player Episode Cleanup - Episodes that aren\'t in the queue and aren\'t favorites should be eligible for removal if Auto Download needs space for new episodes + Episodes that should be eligible for removal if Auto Download needs space for new episodes Pause playback when headphones or bluetooth are disconnected Resume playback when the headphones are reconnected Resume playback when bluetooth reconnects diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java new file mode 100644 index 000000000..8c02391ca --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.core.storage; + +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.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.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); + } + + @Test + public void testPerformAutoCleanupHandleUnplayed() throws IOException { + Feed feed = new Feed("url", null, "title"); + List items = new ArrayList<>(); + feed.setItems(items); + List files = new ArrayList<>(); + populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, false, false); + + DBTasks.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 items = new ArrayList<>(); + feed.setItems(items); + List files = new ArrayList<>(); + populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, true, false); + + DBTasks.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 items = new ArrayList<>(); + feed.setItems(items); + List files = new ArrayList<>(); + populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, false, true); + + DBTasks.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 + } +}