diff --git a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java index a19b5e3c9..10666aa36 100644 --- a/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java +++ b/app/src/main/java/de/danoeh/antennapod/config/ClientConfigurator.java @@ -16,5 +16,6 @@ public class ClientConfigurator { ClientConfig.playbackServiceCallbacks = new PlaybackServiceCallbacksImpl(); ClientConfig.storageCallbacks = new StorageCallbacksImpl(); ClientConfig.flattrCallbacks = new FlattrCallbacksImpl(); + ClientConfig.dbTasksCallbacks = new DBTasksCallbacksImpl(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/config/DBTasksCallbacksImpl.java b/app/src/main/java/de/danoeh/antennapod/config/DBTasksCallbacksImpl.java new file mode 100644 index 000000000..75dcb2ef1 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/config/DBTasksCallbacksImpl.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.config; + +import de.danoeh.antennapod.core.DBTasksCallbacks; +import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.APDownloadAlgorithm; +import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; +import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; + +public class DBTasksCallbacksImpl implements DBTasksCallbacks { + + @Override + public AutomaticDownloadAlgorithm getAutomaticDownloadAlgorithm() { + return new APDownloadAlgorithm(); + } + + @Override + public EpisodeCleanupAlgorithm getEpisodeCacheCleanupAlgorithm() { + return new APCleanupAlgorithm(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java index e5e609f5f..1a2671555 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java @@ -22,4 +22,6 @@ public class ClientConfig { public static FlattrCallbacks flattrCallbacks; public static StorageCallbacks storageCallbacks; + + public static DBTasksCallbacks dbTasksCallbacks; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java new file mode 100644 index 000000000..edf3e3199 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core; + +import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; +import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; + +/** + * Callbacks for the DBTasks class of the storage module. + */ +public interface DBTasksCallbacks { + + /** + * Returns the client's implementation of the AutomaticDownloadAlgorithm interface. + */ + public AutomaticDownloadAlgorithm getAutomaticDownloadAlgorithm(); + + /** + * Returns the client's implementation of the EpisodeCacheCleanupAlgorithm interface. + */ + public EpisodeCleanupAlgorithm getEpisodeCacheCleanupAlgorithm(); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java new file mode 100644 index 000000000..499fddf74 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java @@ -0,0 +1,103 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.QueueAccess; + +/** + * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod. + */ +public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm { + private static final String TAG = "APCleanupAlgorithm"; + + @Override + public int performCleanup(Context context, Integer episodeNumber) { + List candidates = new ArrayList(); + List downloadedItems = DBReader.getDownloadedItems(context); + QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context)); + List delete; + for (FeedItem item : downloadedItems) { + if (item.hasMedia() && item.getMedia().isDownloaded() + && !queue.contains(item.getId()) && item.isRead()) { + candidates.add(item); + } + + } + + Collections.sort(candidates, new Comparator() { + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + Date l = lhs.getMedia().getPlaybackCompletionDate(); + Date r = rhs.getMedia().getPlaybackCompletionDate(); + + if (l == null) { + l = new Date(0); + } + if (r == null) { + r = new Date(0); + } + return l.compareTo(r); + } + }); + + if (candidates.size() > episodeNumber) { + delete = candidates.subList(0, episodeNumber); + } else { + delete = candidates; + } + + for (FeedItem item : delete) { + try { + DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + int counter = delete.size(); + + + Log.i(TAG, String.format( + "Auto-delete deleted %d episodes (%d requested)", counter, + episodeNumber)); + + return counter; + } + + @Override + public Integer getDefaultCleanupParameter(Context context) { + return 0; + } + + @Override + public Integer getPerformCleanupParameter(Context context, List items) { + return getPerformAutoCleanupArgs(context, items.size()); + } + + static int getPerformAutoCleanupArgs(Context context, + final int episodeNumber) { + if (episodeNumber >= 0 + && UserPreferences.getEpisodeCacheSize() != UserPreferences + .getEpisodeCacheSizeUnlimited()) { + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(context); + if (downloadedEpisodes + episodeNumber >= UserPreferences + .getEpisodeCacheSize()) { + + return downloadedEpisodes + episodeNumber + - UserPreferences.getEpisodeCacheSize(); + } + } + return 0; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java new file mode 100644 index 000000000..c5f871f48 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java @@ -0,0 +1,133 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.PowerUtils; + +/** + * Implements the automatic download algorithm used by AntennaPod. This class assumes that + * the client uses the APEpisodeCleanupAlgorithm. + */ +public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { + private static final String TAG = "APDownloadAlgorithm"; + + private final APCleanupAlgorithm cleanupAlgorithm = new APCleanupAlgorithm(); + + /** + * Looks for undownloaded 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. + * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if + * its media ID is in the mediaIds list. + * @return A Runnable that will be submitted to an ExecutorService. + */ + @Override + public Runnable autoDownloadUndownloadedItems(final Context context, final long... mediaIds) { + return new Runnable() { + @Override + public void run() { + + // true if we should auto download based on network status + boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable(context) + && UserPreferences.isEnableAutodownload(); + + // true if we should auto download based on power status + boolean powerShouldAutoDl = PowerUtils.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"); + + final List queue = DBReader.getQueue(context); + final List unreadItems = DBReader + .getUnreadItemsList(context); + + int undownloadedEpisodes = DBTasks.getNumberOfUndownloadedEpisodes(queue, + unreadItems); + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(context); + int deletedEpisodes = cleanupAlgorithm.performCleanup(context, + APCleanupAlgorithm.getPerformAutoCleanupArgs(context, undownloadedEpisodes)); + int episodeSpaceLeft = undownloadedEpisodes; + boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences + .getEpisodeCacheSizeUnlimited(); + + if (!cacheIsUnlimited + && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes + + undownloadedEpisodes) { + episodeSpaceLeft = UserPreferences.getEpisodeCacheSize() + - (downloadedEpisodes - deletedEpisodes); + } + + Arrays.sort(mediaIds); // sort for binary search + final boolean ignoreMediaIds = mediaIds.length == 0; + List itemsToDownload = new ArrayList(); + + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { + for (int i = 0; i < queue.size(); i++) { // ignore playing item + FeedItem item = queue.get(i); + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; + if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) + && item.hasMedia() + && !item.getMedia().isDownloaded() + && !item.getMedia().isPlaying() + && item.getFeed().getPreferences().getAutoDownload()) { + itemsToDownload.add(item); + episodeSpaceLeft--; + undownloadedEpisodes--; + if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { + break; + } + } + } + } + + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { + for (FeedItem item : unreadItems) { + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; + if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) + && item.hasMedia() + && !item.getMedia().isDownloaded() + && item.getFeed().getPreferences().getAutoDownload()) { + itemsToDownload.add(item); + episodeSpaceLeft--; + undownloadedEpisodes--; + if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { + break; + } + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Enqueueing " + itemsToDownload.size() + + " items for download"); + + try { + DBTasks.downloadFeedItems(false, context, + itemsToDownload.toArray(new FeedItem[itemsToDownload + .size()]) + ); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + + } + } + }; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java new file mode 100644 index 000000000..b53645d93 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java @@ -0,0 +1,136 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.util.Log; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedItem; + +/** + * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPodSP apps. + */ +public class APSPCleanupAlgorithm implements EpisodeCleanupAlgorithm { + private static final String TAG = "APSPCleanupAlgorithm"; + + final int numberOfNewAutomaticallyDownloadedEpisodes; + + public APSPCleanupAlgorithm(int numberOfNewAutomaticallyDownloadedEpisodes) { + this.numberOfNewAutomaticallyDownloadedEpisodes = numberOfNewAutomaticallyDownloadedEpisodes; + } + + /** + * Performs an automatic cleanup. Episodes that have been downloaded first will also be deleted first. + * The episode that is currently playing as well as the n most recent episodes (the exact value is determined + * by AppPreferences.numberOfNewAutomaticallyDownloadedEpisodes) will never be deleted. + * + * @param context + * @param episodeSize The maximum amount of space that should be freed by this method + * @return The number of episodes that have been deleted + */ + @Override + public int performCleanup(Context context, Integer episodeSize) { + if (BuildConfig.DEBUG) Log.d(TAG, String.format("performAutoCleanup(%d)", episodeSize)); + if (episodeSize <= 0) { + return 0; + } + + List candidates = getAutoCleanupCandidates(context); + List deleteList = new ArrayList(); + long deletedEpisodesSize = 0; + Collections.sort(candidates, new Comparator() { + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + File lFile = new File(lhs.getMedia().getFile_url()); + File rFile = new File(rhs.getMedia().getFile_url()); + if (!lFile.exists() || !rFile.exists()) { + return 0; + } + if (FileUtils.isFileOlder(lFile, rFile)) { + return -1; + } else { + return 1; + } + } + }); + // listened episodes will be deleted first + Iterator it = candidates.iterator(); + for (FeedItem i = it.next(); it.hasNext() && deletedEpisodesSize <= episodeSize; i = it.next()) { + if (!i.getMedia().isPlaying() && i.getMedia().getPlaybackCompletionDate() != null) { + it.remove(); + deleteList.add(i); + deletedEpisodesSize += i.getMedia().getSize(); + } + } + // delete unlistened old episodes if necessary + it = candidates.iterator(); + for (FeedItem i = it.next(); it.hasNext() && deletedEpisodesSize <= episodeSize; i = it.next()) { + if (!i.getMedia().isPlaying()) { + it.remove(); + deleteList.add(i); + deletedEpisodesSize += i.getMedia().getSize(); + } + } + for (FeedItem item : deleteList) { + try { + DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, String.format("performAutoCleanup(%d) deleted %d episodes and free'd %d bytes of memory", + episodeSize, deleteList.size(), deletedEpisodesSize)); + return deleteList.size(); + } + + @Override + public Integer getDefaultCleanupParameter(Context context) { + return 0; + } + + @Override + public Integer getPerformCleanupParameter(Context context, List items) { + int episodeSize = 0; + for (FeedItem item : items) { + if (item.hasMedia() && !item.getMedia().isDownloaded()) { + episodeSize += item.getMedia().getSize(); + } + } + return episodeSize; + } + + /** + * Returns list of FeedItems that have been downloaded, but are not one of the + * [numberOfNewAutomaticallyDownloadedEpisodes] most recent items. + */ + private List getAutoCleanupCandidates(Context context) { + List downloaded = new ArrayList(DBReader.getDownloadedItems(context)); + List recent = new ArrayList(DBReader.getRecentlyPublishedEpisodes(context, + numberOfNewAutomaticallyDownloadedEpisodes)); + for (FeedItem r : recent) { + if (r.hasMedia() && r.getMedia().isDownloaded()) { + for (int i = 0; i < downloaded.size(); i++) { + if (downloaded.get(i).getId() == r.getId()) { + downloaded.remove(i); + break; + } + } + } + } + + return downloaded; + + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java new file mode 100644 index 000000000..feeca776e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java @@ -0,0 +1,66 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.util.Log; + +import java.util.Iterator; +import java.util.List; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.util.NetworkUtils; + +/** + * Implements the automatic download algorithm used by AntennaPodSP apps. + */ +public class APSPDownloadAlgorithm implements AutomaticDownloadAlgorithm { + private static final String TAG = "APSPDownloadAlgorithm"; + + private final int numberOfNewAutomaticallyDownloadedEpisodes; + + public APSPDownloadAlgorithm(int numberOfNewAutomaticallyDownloadedEpisodes) { + this.numberOfNewAutomaticallyDownloadedEpisodes = numberOfNewAutomaticallyDownloadedEpisodes; + } + + /** + * Downloads the most recent episodes automatically. The exact number of + * episodes that will be downloaded can be set in the AppPreferences. + * + * @param context Used for accessing the DB. + * @return A Runnable that will be submitted to an ExecutorService. + */ + @Override + public Runnable autoDownloadUndownloadedItems(final Context context, final long... mediaIds) { + return new Runnable() { + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Performing auto-dl of undownloaded episodes"); + if (NetworkUtils.autodownloadNetworkAvailable(context) + && UserPreferences.isEnableAutodownload()) { + List itemsToDownload = DBReader.getRecentlyPublishedEpisodes(context, + numberOfNewAutomaticallyDownloadedEpisodes); + Iterator it = itemsToDownload.iterator(); + for (FeedItem item = it.next(); it.hasNext(); item = it.next()) { + if (item.getMedia().isDownloaded()) { + it.remove(); + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Enqueueing " + itemsToDownload.size() + + " items for automatic download"); + if (!itemsToDownload.isEmpty()) { + try { + DBTasks.downloadFeedItems(false, context, + itemsToDownload.toArray(new FeedItem[itemsToDownload + .size()])); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + } + } + }; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java new file mode 100644 index 000000000..9ca9620a7 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; + +public interface AutomaticDownloadAlgorithm { + + /** + * Looks for undownloaded episodes 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. + * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if + * its media ID is in the mediaIds list. + * @return A Runnable that will be submitted to an ExecutorService. + */ + public Runnable autoDownloadUndownloadedItems(Context context, long... mediaIds); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index 96baeeb96..e73f9599d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -8,7 +8,6 @@ import android.util.Log; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.Iterator; import java.util.List; @@ -35,8 +34,6 @@ import de.danoeh.antennapod.core.service.GpodnetSyncService; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.util.DownloadError; -import de.danoeh.antennapod.core.util.NetworkUtils; -import de.danoeh.antennapod.core.util.PowerUtils; import de.danoeh.antennapod.core.util.QueueAccess; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException; @@ -387,8 +384,8 @@ public final class DBTasks { downloadFeedItems(true, context, items); } - private static void downloadFeedItems(boolean performAutoCleanup, - final Context context, final FeedItem... items) + static void downloadFeedItems(boolean performAutoCleanup, + final Context context, final FeedItem... items) throws DownloadRequestException { final DownloadRequester requester = DownloadRequester.getInstance(); @@ -397,8 +394,10 @@ public final class DBTasks { @Override public void run() { - performAutoCleanup(context, - getPerformAutoCleanupArgs(context, items.length)); + ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm() + .performCleanup(context, + ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm() + .getPerformCleanupParameter(context, Arrays.asList(items))); } }.start(); @@ -428,7 +427,7 @@ public final class DBTasks { } } - private static int getNumberOfUndownloadedEpisodes( + static int getNumberOfUndownloadedEpisodes( final List queue, final List unreadItems) { int counter = 0; for (FeedItem item : queue) { @@ -460,117 +459,9 @@ public final class DBTasks { * @return A Future that can be used for waiting for the methods completion. */ public static Future autodownloadUndownloadedItems(final Context context, final long... mediaIds) { - return autodownloadExec.submit(new Runnable() { - @Override - public void run() { + return autodownloadExec.submit(ClientConfig.dbTasksCallbacks.getAutomaticDownloadAlgorithm() + .autoDownloadUndownloadedItems(context, mediaIds)); - // true if we should auto download based on network status - boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable(context) - && UserPreferences.isEnableAutodownload(); - - // true if we should auto download based on power status - boolean powerShouldAutoDl = PowerUtils.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"); - - final List queue = DBReader.getQueue(context); - final List unreadItems = DBReader - .getUnreadItemsList(context); - - int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(queue, - unreadItems); - int downloadedEpisodes = DBReader - .getNumberOfDownloadedEpisodes(context); - int deletedEpisodes = performAutoCleanup(context, - getPerformAutoCleanupArgs(context, undownloadedEpisodes)); - int episodeSpaceLeft = undownloadedEpisodes; - boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences - .getEpisodeCacheSizeUnlimited(); - - if (!cacheIsUnlimited - && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes - + undownloadedEpisodes) { - episodeSpaceLeft = UserPreferences.getEpisodeCacheSize() - - (downloadedEpisodes - deletedEpisodes); - } - - Arrays.sort(mediaIds); // sort for binary search - final boolean ignoreMediaIds = mediaIds.length == 0; - List itemsToDownload = new ArrayList(); - - if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { - for (int i = 0; i < queue.size(); i++) { // ignore playing item - FeedItem item = queue.get(i); - long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; - if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) - && item.hasMedia() - && !item.getMedia().isDownloaded() - && !item.getMedia().isPlaying() - && item.getFeed().getPreferences().getAutoDownload()) { - itemsToDownload.add(item); - episodeSpaceLeft--; - undownloadedEpisodes--; - if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { - break; - } - } - } - } - - if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { - for (FeedItem item : unreadItems) { - long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; - if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) - && item.hasMedia() - && !item.getMedia().isDownloaded() - && item.getFeed().getPreferences().getAutoDownload()) { - itemsToDownload.add(item); - episodeSpaceLeft--; - undownloadedEpisodes--; - if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { - break; - } - } - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Enqueueing " + itemsToDownload.size() - + " items for download"); - - try { - downloadFeedItems(false, context, - itemsToDownload.toArray(new FeedItem[itemsToDownload - .size()]) - ); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - - } - } - }); - - } - - private static int getPerformAutoCleanupArgs(Context context, - final int episodeNumber) { - if (episodeNumber >= 0 - && UserPreferences.getEpisodeCacheSize() != UserPreferences - .getEpisodeCacheSizeUnlimited()) { - int downloadedEpisodes = DBReader - .getNumberOfDownloadedEpisodes(context); - if (downloadedEpisodes + episodeNumber >= UserPreferences - .getEpisodeCacheSize()) { - - return downloadedEpisodes + episodeNumber - - UserPreferences.getEpisodeCacheSize(); - } - } - return 0; } /** @@ -582,63 +473,8 @@ public final class DBTasks { * @param context Used for accessing the DB. */ public static void performAutoCleanup(final Context context) { - performAutoCleanup(context, getPerformAutoCleanupArgs(context, 0)); - } - - private static int performAutoCleanup(final Context context, - final int episodeNumber) { - List candidates = new ArrayList(); - List downloadedItems = DBReader.getDownloadedItems(context); - QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context)); - List delete; - for (FeedItem item : downloadedItems) { - if (item.hasMedia() && item.getMedia().isDownloaded() - && !queue.contains(item.getId()) && item.isRead()) { - candidates.add(item); - } - - } - - Collections.sort(candidates, new Comparator() { - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - Date l = lhs.getMedia().getPlaybackCompletionDate(); - Date r = rhs.getMedia().getPlaybackCompletionDate(); - - if (l == null) { - l = new Date(0); - } - if (r == null) { - r = new Date(0); - } - return l.compareTo(r); - } - }); - - if (candidates.size() > episodeNumber) { - delete = candidates.subList(0, episodeNumber); - } else { - delete = candidates; - } - - for (FeedItem item : delete) { - try { - DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - int counter = delete.size(); - - if (BuildConfig.DEBUG) - Log.d(TAG, String.format( - "Auto-delete deleted %d episodes (%d requested)", counter, - episodeNumber)); - - return counter; + ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().performCleanup(context, + ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().getDefaultCleanupParameter(context)); } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java new file mode 100644 index 000000000..6a8b4a441 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; + +import java.util.List; + +import de.danoeh.antennapod.core.feed.FeedItem; + +public interface 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 parameter An additional parameter. This parameter is either returned by getDefaultCleanupParameter + * or getPerformCleanupParameter. + * @return The number of episodes that were deleted. + */ + public int performCleanup(Context context, T parameter); + + /** + * 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. + */ + public T getDefaultCleanupParameter(Context context); + + /** + * Returns a parameter for performCleanup. + * + * @param items A list of FeedItems that are about to be downloaded. The implementation of this interface + * should decide how much space to free to satisfy the episode cache conditions. + */ + public T getPerformCleanupParameter(Context context, List items); +}