diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2024-04-04 22:26:53 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-04 22:26:53 +0200 |
commit | 2143ab135182434911d4554a8ef08115eaa0d2d0 (patch) | |
tree | b380fdf38b34a032c5ea0ed5b5fa22dce7433723 /net/download | |
parent | 0288d4e51eb7eef565be8d814fb8c152383e5031 (diff) | |
download | AntennaPod-2143ab135182434911d4554a8ef08115eaa0d2d0.zip |
Move some tests from core module to their respective module (#7059)
Diffstat (limited to 'net/download')
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 + } +} |