diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2024-03-29 17:45:14 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-29 17:45:14 +0100 |
commit | 6f3a9b16764a57e43994ccbeeada5224dee93f44 (patch) | |
tree | 24bd2a3ed120dd61b009e5a2b5b9a1c550579997 /core/src/main | |
parent | 0c8c9a89a371d6515c34d1c4f7417c26059ee969 (diff) | |
download | AntennaPod-6f3a9b16764a57e43994ccbeeada5224dee93f44.zip |
Create module for sync service and move DBWriter to database module (#7040)
Diffstat (limited to 'core/src/main')
34 files changed, 57 insertions, 2384 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfigurator.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfigurator.java index 48d937266..8b5f9f286 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfigurator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfigurator.java @@ -3,6 +3,12 @@ package de.danoeh.antennapod.core; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import de.danoeh.antennapod.core.storage.AutoDownloadManagerImpl; +import de.danoeh.antennapod.core.util.download.FeedUpdateManagerImpl; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.net.sync.service.SyncService; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; import de.danoeh.antennapod.storage.preferences.SynchronizationCredentials; import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; @@ -13,8 +19,6 @@ import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.net.common.AntennapodHttpClient; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.core.service.download.DownloadServiceInterfaceImpl; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.net.common.NetworkUtils; import de.danoeh.antennapod.core.util.download.NetworkConnectionChangeHandler; import de.danoeh.antennapod.net.ssl.SslProviderInstaller; @@ -46,6 +50,8 @@ public class ClientConfigurator { NetworkUtils.init(context); NetworkConnectionChangeHandler.init(context); DownloadServiceInterface.setImpl(new DownloadServiceInterfaceImpl()); + FeedUpdateManager.setInstance(new FeedUpdateManagerImpl()); + AutoDownloadManager.setInstance(new AutoDownloadManagerImpl()); SynchronizationQueueSink.setServiceStarterImpl(() -> SyncService.sync(context)); AntennapodHttpClient.setCacheDirectory(new File(context.getCacheDir(), "okhttp")); AntennapodHttpClient.setProxyConfig(UserPreferences.getProxyConfig()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java index 358b75e9c..b30f657a1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -8,8 +8,8 @@ import android.content.Context; import android.os.ParcelFileDescriptor; import android.util.Log; -import de.danoeh.antennapod.core.storage.FeedDatabaseWriter; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; import de.danoeh.antennapod.storage.importexport.OpmlElement; import de.danoeh.antennapod.storage.importexport.OpmlReader; import de.danoeh.antennapod.storage.importexport.OpmlWriter; @@ -148,7 +148,7 @@ public class OpmlBackupAgent extends BackupAgentHelper { feed.setItems(Collections.emptyList()); FeedDatabaseWriter.updateFeed(mContext, feed, false); } - FeedUpdateManager.runOnce(mContext); + FeedUpdateManager.getInstance().runOnce(mContext); } catch (XmlPullParserException e) { Log.e(TAG, "Error while parsing the OPML file", e); } catch (IOException e) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index a4dab1996..8230924f9 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -31,8 +31,8 @@ import de.danoeh.antennapod.core.util.FastDocumentFile; import de.danoeh.antennapod.model.MediaMetadataRetrieverCompat; import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.FeedDatabaseWriter; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.parser.feed.util.DateUtils; import de.danoeh.antennapod.model.download.DownloadError; import de.danoeh.antennapod.model.feed.Feed; diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java index e30b49280..098c9bfa4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.util.Log; import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; /** * Refreshes all feeds when it receives an intent @@ -20,7 +20,7 @@ public class FeedUpdateReceiver extends BroadcastReceiver { Log.d(TAG, "Received intent"); ClientConfigurator.initialize(context); - FeedUpdateManager.runOnce(context); + FeedUpdateManager.getInstance().runOnce(context); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java index 45b6aee04..e5828ac6e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/FeedUpdateWorker.java @@ -24,12 +24,12 @@ import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.NewEpisodesNotification; import de.danoeh.antennapod.core.service.download.handler.FeedParserTask; -import de.danoeh.antennapod.core.storage.AutoDownloadManager; +import de.danoeh.antennapod.core.util.download.FeedUpdateManagerImpl; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.FeedDatabaseWriter; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.net.common.NetworkUtils; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.model.download.DownloadError; import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.model.feed.Feed; @@ -62,7 +62,7 @@ public class FeedUpdateWorker extends Worker { newEpisodesNotification.loadCountersBeforeRefresh(); List<Feed> toUpdate; - long feedId = getInputData().getLong(FeedUpdateManager.EXTRA_FEED_ID, -1); + long feedId = getInputData().getLong(FeedUpdateManagerImpl.EXTRA_FEED_ID, -1); boolean allAreLocal = true; boolean force = false; if (feedId == -1) { // Update all @@ -91,7 +91,7 @@ public class FeedUpdateWorker extends Worker { force = true; } - if (!getInputData().getBoolean(FeedUpdateManager.EXTRA_EVEN_ON_MOBILE, false) && !allAreLocal) { + if (!getInputData().getBoolean(FeedUpdateManagerImpl.EXTRA_EVEN_ON_MOBILE, false) && !allAreLocal) { if (!NetworkUtils.networkAvailable() || !NetworkUtils.isFeedRefreshAllowed()) { Log.d(TAG, "Blocking automatic update"); return Result.retry(); @@ -100,7 +100,7 @@ public class FeedUpdateWorker extends Worker { refreshFeeds(toUpdate, force); notificationManager.cancel(R.id.notification_updating_feeds); - AutoDownloadManager.autodownloadUndownloadedItems(getApplicationContext()); + AutoDownloadManager.getInstance().autodownloadUndownloadedItems(getApplicationContext()); return Result.success(); } @@ -164,7 +164,7 @@ public class FeedUpdateWorker extends Worker { } void refreshFeed(Feed feed, boolean force) throws Exception { - boolean nextPage = getInputData().getBoolean(FeedUpdateManager.EXTRA_NEXT_PAGE, false) + boolean nextPage = getInputData().getBoolean(FeedUpdateManagerImpl.EXTRA_NEXT_PAGE, false) && feed.getNextPageLink() != null; if (nextPage) { feed.setPageNr(feed.getPageNr() + 1); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java index 6c595388a..e2489b493 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceInterfaceImpl.java @@ -9,7 +9,7 @@ import androidx.work.OneTimeWorkRequest; import androidx.work.OutOfQuotaPolicy; import androidx.work.WorkInfo; import androidx.work.WorkManager; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java index 9c073713f..a2b4ed100 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/EpisodeDownloadWorker.java @@ -23,7 +23,7 @@ import de.danoeh.antennapod.core.ClientConfigurator; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.service.download.handler.MediaDownloadedHandler; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.model.download.DownloadError; import de.danoeh.antennapod.model.download.DownloadResult; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java index 62ce395f7..24b157c88 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/MediaDownloadedHandler.java @@ -7,6 +7,7 @@ import android.util.Log; import androidx.annotation.NonNull; import de.danoeh.antennapod.model.MediaMetadataRetrieverCompat; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import org.greenrobot.eventbus.EventBus; import java.io.File; @@ -17,8 +18,7 @@ import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; import de.danoeh.antennapod.model.download.DownloadRequest; import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.model.download.DownloadError; import de.danoeh.antennapod.model.feed.FeedItem; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 6f7068d24..4cd4931e1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -51,6 +51,7 @@ import androidx.core.content.ContextCompat; import androidx.media.MediaBrowserServiceCompat; import de.danoeh.antennapod.event.PlayerStatusEvent; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import de.danoeh.antennapod.ui.notifications.NotificationUtils; import de.danoeh.antennapod.ui.widget.WidgetUpdater; import org.greenrobot.eventbus.EventBus; @@ -71,10 +72,8 @@ import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.service.QuickSettingsTileService; import de.danoeh.antennapod.core.service.playback.PlaybackServiceTaskManager.SleepTimer; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.core.util.ChapterUtils; -import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.FeedUtil; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.net.common.NetworkUtils; @@ -1110,8 +1109,10 @@ public class PlaybackService extends MediaBrowserServiceCompat { } FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); - boolean smartMarkAsPlayed = FeedItemUtil.hasAlmostEnded(media); - if (!ended && smartMarkAsPlayed) { + int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); + boolean almostEnded = media.getDuration() > 0 + && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000; + if (!ended && almostEnded) { Log.d(TAG, "smart mark as played"); } @@ -1121,7 +1122,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { autoSkipped = true; } - if (ended || smartMarkAsPlayed) { + if (ended || almostEnded) { SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( getApplicationContext(), media, true); media.onPlaybackCompleted(getApplicationContext()); @@ -1132,11 +1133,11 @@ public class PlaybackService extends MediaBrowserServiceCompat { } if (item != null) { - if (ended || smartMarkAsPlayed + if (ended || almostEnded || autoSkipped || (skipped && !UserPreferences.shouldSkipKeepEpisode())) { // only mark the item as played if we're not keeping it anyways - DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended || (skipped && smartMarkAsPlayed)); + DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended || (skipped && almostEnded)); // don't know if it actually matters to not autodownload when smart mark as played is triggered DBWriter.removeQueueItem(PlaybackService.this, ended, item); // Delete episode if enabled 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 index 49170c40f..d1de1f616 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java @@ -18,6 +18,7 @@ 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. diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java index cd24ca03d..74ee43cf0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java @@ -15,6 +15,7 @@ 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 diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManager.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManagerImpl.java index bc5244381..b00375ffe 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManagerImpl.java @@ -2,12 +2,13 @@ package de.danoeh.antennapod.core.storage; 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 abstract class AutoDownloadManager { +public class AutoDownloadManagerImpl extends AutoDownloadManager { private static final String TAG = "AutoDownloadManager"; /** @@ -35,7 +36,7 @@ public abstract class AutoDownloadManager { * @param context Used for accessing the DB. * @return A Future that can be used for waiting for the methods completion. */ - public static Future<?> autodownloadUndownloadedItems(final Context context) { + public Future<?> autodownloadUndownloadedItems(final Context context) { Log.d(TAG, "autodownloadUndownloadedItems"); return autodownloadExec.submit(downloadAlgorithm.autoDownloadUndownloadedItems(context)); } @@ -48,7 +49,7 @@ public abstract class AutoDownloadManager { * * @param context Used for accessing the DB. */ - public static void performAutoCleanup(final Context context) { + public void performAutoCleanup(final Context context) { EpisodeCleanupAlgorithmFactory.build().performCleanup(context); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java deleted file mode 100644 index 1e5b416e2..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ /dev/null @@ -1,1065 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import android.app.backup.BackupManager; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationManagerCompat; -import androidx.documentfile.provider.DocumentFile; - -import com.google.common.util.concurrent.Futures; - -import de.danoeh.antennapod.event.DownloadLogEvent; -import de.danoeh.antennapod.core.feed.LocalFeedUpdater; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.core.service.playback.PlaybackServiceInterface; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.PodDBAdapter; - -import org.greenrobot.eventbus.EventBus; - -import java.io.File; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.event.FavoritesEvent; -import de.danoeh.antennapod.event.FeedItemEvent; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.event.playback.PlaybackHistoryEvent; -import de.danoeh.antennapod.event.QueueEvent; -import de.danoeh.antennapod.event.UnreadItemsUpdateEvent; -import de.danoeh.antennapod.event.FeedEvent; -import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; -import de.danoeh.antennapod.storage.database.FeedItemPermutors; -import de.danoeh.antennapod.core.util.IntentUtils; -import de.danoeh.antennapod.storage.database.LongList; -import de.danoeh.antennapod.storage.database.Permutor; -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.model.feed.FeedPreferences; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.model.playback.Playable; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; - -/** - * Provides methods for writing data to AntennaPod's database. - * In general, DBWriter-methods will be executed on an internal ExecutorService. - * Some methods return a Future-object which the caller can use for waiting for the method's completion. The returned Future's - * will NOT contain any results. - */ -public class DBWriter { - - private static final String TAG = "DBWriter"; - - private static final ExecutorService dbExec; - - static { - dbExec = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r); - t.setName("DatabaseExecutor"); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }); - } - - private DBWriter() { - } - - /** - * Wait until all threads are finished to avoid the "Illegal connection pointer" error of - * Robolectric. Call this method only for unit tests. - */ - public static void tearDownTests() { - try { - dbExec.awaitTermination(1, TimeUnit.SECONDS); - } catch (InterruptedException e) { - // ignore error - } - } - - /** - * Deletes a downloaded FeedMedia file from the storage device. - * - * @param context A context that is used for opening a database connection. - */ - public static Future<?> deleteFeedMediaOfItem(@NonNull final Context context, - final FeedMedia media) { - return runOnDbThread(() -> { - if (media == null) { - return; - } - boolean result = deleteFeedMediaSynchronous(context, media); - if (result && UserPreferences.shouldDeleteRemoveFromQueue()) { - DBWriter.removeQueueItemSynchronous(context, false, media.getItemId()); - } - }); - } - - private static boolean deleteFeedMediaSynchronous(@NonNull Context context, @NonNull FeedMedia media) { - Log.i(TAG, String.format(Locale.US, "Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s", - media.getId(), media.getEpisodeTitle(), media.isDownloaded())); - boolean localDelete = false; - if (media.getLocalFileUrl() != null && media.getLocalFileUrl().startsWith("content://")) { - // Local feed - DocumentFile documentFile = DocumentFile.fromSingleUri(context, Uri.parse(media.getLocalFileUrl())); - if (documentFile == null || !documentFile.exists() || !documentFile.delete()) { - EventBus.getDefault().post(new MessageEvent(context.getString(R.string.delete_local_failed))); - return false; - } - media.setLocalFileUrl(null); - localDelete = true; - } else if (media.getLocalFileUrl() != null) { - // delete downloaded media file - File mediaFile = new File(media.getLocalFileUrl()); - if (mediaFile.exists() && !mediaFile.delete()) { - MessageEvent evt = new MessageEvent(context.getString(R.string.delete_failed)); - EventBus.getDefault().post(evt); - return false; - } - media.setDownloaded(false); - media.setLocalFileUrl(null); - media.setHasEmbeddedPicture(false); - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setMedia(media); - adapter.close(); - } - - if (media.getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()) { - PlaybackPreferences.writeNoMediaPlaying(); - IntentUtils.sendLocalBroadcast(context, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE); - - NotificationManagerCompat nm = NotificationManagerCompat.from(context); - nm.cancel(R.id.notification_playing); - } - - if (localDelete) { - // Do full update of this feed to get rid of the item - LocalFeedUpdater.updateFeed(media.getItem().getFeed(), context.getApplicationContext(), null); - } else { - // Gpodder: queue delete action for synchronization - FeedItem item = media.getItem(); - EpisodeAction action = new EpisodeAction.Builder(item, EpisodeAction.DELETE) - .currentTimestamp() - .build(); - SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); - - EventBus.getDefault().post(FeedItemEvent.updated(media.getItem())); - } - return true; - } - - /** - * Deletes a Feed and all downloaded files of its components like images and downloaded episodes. - * - * @param context A context that is used for opening a database connection. - * @param feedId ID of the Feed that should be deleted. - */ - public static Future<?> deleteFeed(final Context context, final long feedId) { - return runOnDbThread(() -> { - final Feed feed = DBReader.getFeed(feedId, false); - if (feed == null) { - return; - } - - deleteFeedItemsSynchronous(context, feed.getItems()); - - // delete feed - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.removeFeed(feed); - adapter.close(); - - if (!feed.isLocalFeed()) { - SynchronizationQueueSink.enqueueFeedRemovedIfSynchronizationIsActive(context, feed.getDownloadUrl()); - } - EventBus.getDefault().post(new FeedListUpdateEvent(feed)); - }); - } - - /** - * Remove the listed items and their FeedMedia entries. - * Deleting media also removes the download log entries. - */ - @NonNull - public static Future<?> deleteFeedItems(@NonNull Context context, @NonNull List<FeedItem> items) { - return runOnDbThread(() -> deleteFeedItemsSynchronous(context, items)); - } - - /** - * Remove the listed items and their FeedMedia entries. - * Deleting media also removes the download log entries. - */ - private static void deleteFeedItemsSynchronous(@NonNull Context context, @NonNull List<FeedItem> items) { - List<FeedItem> queue = DBReader.getQueue(); - List<FeedItem> removedFromQueue = new ArrayList<>(); - for (FeedItem item : items) { - if (queue.remove(item)) { - removedFromQueue.add(item); - } - if (item.getMedia() != null) { - if (item.getMedia().getId() == PlaybackPreferences.getCurrentlyPlayingFeedMediaId()) { - // Applies to both downloaded and streamed media - PlaybackPreferences.writeNoMediaPlaying(); - IntentUtils.sendLocalBroadcast(context, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE); - } - if (!item.getFeed().isLocalFeed()) { - if (DownloadServiceInterface.get().isDownloadingEpisode(item.getMedia().getDownloadUrl())) { - DownloadServiceInterface.get().cancel(context, item.getMedia()); - } - if (item.getMedia().isDownloaded()) { - deleteFeedMediaSynchronous(context, item.getMedia()); - } - } - } - } - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - if (!removedFromQueue.isEmpty()) { - adapter.setQueue(queue); - } - adapter.removeFeedItems(items); - adapter.close(); - - for (FeedItem item : removedFromQueue) { - EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item)); - } - - // we assume we also removed download log entries for the feed or its media files. - // especially important if download or refresh failed, as the user should not be able - // to retry these - EventBus.getDefault().post(DownloadLogEvent.listUpdated()); - - BackupManager backupManager = new BackupManager(context); - backupManager.dataChanged(); - } - - /** - * Deletes the entire playback history. - */ - public static Future<?> clearPlaybackHistory() { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.clearPlaybackHistory(); - adapter.close(); - EventBus.getDefault().post(PlaybackHistoryEvent.listUpdated()); - }); - } - - /** - * Deletes the entire download log. - */ - public static Future<?> clearDownloadLog() { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.clearDownloadLog(); - adapter.close(); - EventBus.getDefault().post(DownloadLogEvent.listUpdated()); - }); - } - - public static Future<?> deleteFromPlaybackHistory(FeedItem feedItem) { - return addItemToPlaybackHistory(feedItem.getMedia(), new Date(0)); - } - - /** - * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if - * its playback completion date is set to a non-null value. This method will set the playback completion date to the - * current date regardless of the current value. - * - * @param media FeedMedia that should be added to the playback history. - */ - public static Future<?> addItemToPlaybackHistory(FeedMedia media) { - return addItemToPlaybackHistory(media, new Date()); - } - - /** - * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if - * its playback completion date is set to a non-null value. This method will set the playback completion date to the - * current date regardless of the current value. - * - * @param media FeedMedia that should be added to the playback history. - * @param date PlaybackCompletionDate for <code>media</code> - */ - public static Future<?> addItemToPlaybackHistory(final FeedMedia media, Date date) { - return runOnDbThread(() -> { - Log.d(TAG, "Adding item to playback history"); - media.setPlaybackCompletionDate(date); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedMediaPlaybackCompletionDate(media); - adapter.close(); - EventBus.getDefault().post(PlaybackHistoryEvent.listUpdated()); - - }); - } - - /** - * Adds a Download status object to the download log. - * - * @param status The DownloadStatus object. - */ - public static Future<?> addDownloadStatus(final DownloadResult status) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setDownloadStatus(status); - adapter.close(); - EventBus.getDefault().post(DownloadLogEvent.listUpdated()); - }); - - } - - /** - * Inserts a FeedItem in the queue at the specified index. The 'read'-attribute of the FeedItem will be set to - * true. If the FeedItem is already in the queue, the queue will not be modified. - * - * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem that should be added to the queue. - * @param index Destination index. Must be in range 0..queue.size() - * @param performAutoDownload True if an auto-download process should be started after the operation - * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size() - */ - public static Future<?> addQueueItemAt(final Context context, final long itemId, - final int index, final boolean performAutoDownload) { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(adapter); - FeedItem item; - - if (queue != null) { - if (!itemListContains(queue, itemId)) { - item = DBReader.getFeedItem(itemId); - if (item != null) { - queue.add(index, item); - adapter.setQueue(queue); - item.addTag(FeedItem.TAG_QUEUE); - EventBus.getDefault().post(QueueEvent.added(item, index)); - EventBus.getDefault().post(FeedItemEvent.updated(item)); - if (item.isNew()) { - DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); - } - } - } - } - - adapter.close(); - if (performAutoDownload) { - AutoDownloadManager.autodownloadUndownloadedItems(context); - } - - }); - - } - - public static Future<?> addQueueItem(final Context context, final FeedItem... items) { - return addQueueItem(context, true, items); - } - - public static Future<?> addQueueItem(final Context context, boolean markAsUnplayed, final FeedItem... items) { - LongList itemIds = new LongList(items.length); - for (FeedItem item : items) { - if (!item.hasMedia()) { - continue; - } - itemIds.add(item.getId()); - item.addTag(FeedItem.TAG_QUEUE); - } - return addQueueItem(context, false, markAsUnplayed, itemIds.toArray()); - } - - /** - * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true. - * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue. - * - * @param context A context that is used for opening a database connection. - * @param performAutoDownload true if an auto-download process should be started after the operation. - * @param itemIds IDs of the FeedItem objects that should be added to the queue. - */ - public static Future<?> addQueueItem(final Context context, final boolean performAutoDownload, - final long... itemIds) { - return addQueueItem(context, performAutoDownload, true, itemIds); - } - - /** - * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true. - * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue. - * - * @param context A context that is used for opening a database connection. - * @param performAutoDownload true if an auto-download process should be started after the operation. - * @param markAsUnplayed true if the items should be marked as unplayed when enqueueing - * @param itemIds IDs of the FeedItem objects that should be added to the queue. - */ - public static Future<?> addQueueItem(final Context context, final boolean performAutoDownload, - final boolean markAsUnplayed, final long... itemIds) { - return runOnDbThread(() -> { - if (itemIds.length < 1) { - return; - } - - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(adapter); - - boolean queueModified = false; - LongList markAsUnplayedIds = new LongList(); - List<QueueEvent> events = new ArrayList<>(); - List<FeedItem> updatedItems = new ArrayList<>(); - ItemEnqueuePositionCalculator positionCalculator = - new ItemEnqueuePositionCalculator(UserPreferences.getEnqueueLocation()); - Playable currentlyPlaying = DBReader.getFeedMedia(PlaybackPreferences.getCurrentlyPlayingFeedMediaId()); - int insertPosition = positionCalculator.calcPosition(queue, currentlyPlaying); - for (long itemId : itemIds) { - if (!itemListContains(queue, itemId)) { - final FeedItem item = DBReader.getFeedItem(itemId); - if (item != null) { - queue.add(insertPosition, item); - events.add(QueueEvent.added(item, insertPosition)); - - item.addTag(FeedItem.TAG_QUEUE); - updatedItems.add(item); - queueModified = true; - if (item.isNew()) { - markAsUnplayedIds.add(item.getId()); - } - insertPosition++; - } - } - } - if (queueModified) { - applySortOrder(queue, events); - adapter.setQueue(queue); - for (QueueEvent event : events) { - EventBus.getDefault().post(event); - } - EventBus.getDefault().post(FeedItemEvent.updated(updatedItems)); - if (markAsUnplayed && markAsUnplayedIds.size() > 0) { - DBWriter.markItemPlayed(FeedItem.UNPLAYED, markAsUnplayedIds.toArray()); - } - } - adapter.close(); - if (performAutoDownload) { - AutoDownloadManager.autodownloadUndownloadedItems(context); - } - }); - } - - /** - * Sorts the queue depending on the configured sort order. - * If the queue is not in keep sorted mode, nothing happens. - * - * @param queue The queue to be sorted. - * @param events Replaces the events by a single SORT event if the list has to be sorted automatically. - */ - private static void applySortOrder(List<FeedItem> queue, List<QueueEvent> events) { - if (!UserPreferences.isQueueKeepSorted()) { - // queue is not in keep sorted mode, there's nothing to do - return; - } - - // Sort queue by configured sort order - SortOrder sortOrder = UserPreferences.getQueueKeepSortedOrder(); - if (sortOrder == SortOrder.RANDOM) { - // do not shuffle the list on every change - return; - } - Permutor<FeedItem> permutor = FeedItemPermutors.getPermutor(sortOrder); - permutor.reorder(queue); - - // Replace ADDED events by a single SORTED event - events.clear(); - events.add(QueueEvent.sorted(queue)); - } - - /** - * Removes all FeedItem objects from the queue. - */ - public static Future<?> clearQueue() { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.clearQueue(); - adapter.close(); - - EventBus.getDefault().post(QueueEvent.cleared()); - }); - } - - /** - * Removes a FeedItem object from the queue. - * - * @param context A context that is used for opening a database connection. - * @param performAutoDownload true if an auto-download process should be started after the operation. - * @param item FeedItem that should be removed. - */ - public static Future<?> removeQueueItem(final Context context, - final boolean performAutoDownload, final FeedItem item) { - return runOnDbThread(() -> removeQueueItemSynchronous(context, performAutoDownload, item.getId())); - } - - public static Future<?> removeQueueItem(final Context context, final boolean performAutoDownload, - final long... itemIds) { - return runOnDbThread(() -> removeQueueItemSynchronous(context, performAutoDownload, itemIds)); - } - - private static void removeQueueItemSynchronous(final Context context, - final boolean performAutoDownload, - final long... itemIds) { - if (itemIds.length < 1) { - return; - } - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(adapter); - - if (queue != null) { - boolean queueModified = false; - List<QueueEvent> events = new ArrayList<>(); - List<FeedItem> updatedItems = new ArrayList<>(); - for (long itemId : itemIds) { - int position = indexInItemList(queue, itemId); - if (position >= 0) { - final FeedItem item = DBReader.getFeedItem(itemId); - if (item == null) { - Log.e(TAG, "removeQueueItem - item in queue but somehow cannot be loaded." + - " Item ignored. It should never happen. id:" + itemId); - continue; - } - queue.remove(position); - item.removeTag(FeedItem.TAG_QUEUE); - events.add(QueueEvent.removed(item)); - updatedItems.add(item); - queueModified = true; - } else { - Log.v(TAG, "removeQueueItem - item not in queue:" + itemId); - } - } - if (queueModified) { - adapter.setQueue(queue); - for (QueueEvent event : events) { - EventBus.getDefault().post(event); - } - EventBus.getDefault().post(FeedItemEvent.updated(updatedItems)); - } else { - Log.w(TAG, "Queue was not modified by call to removeQueueItem"); - } - } else { - Log.e(TAG, "removeQueueItem: Could not load queue"); - } - adapter.close(); - if (performAutoDownload) { - AutoDownloadManager.autodownloadUndownloadedItems(context); - } - } - - public static Future<?> toggleFavoriteItem(final FeedItem item) { - if (item.isTagged(FeedItem.TAG_FAVORITE)) { - return removeFavoriteItem(item); - } else { - return addFavoriteItem(item); - } - } - - public static Future<?> addFavoriteItem(final FeedItem item) { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); - adapter.addFavoriteItem(item); - adapter.close(); - item.addTag(FeedItem.TAG_FAVORITE); - EventBus.getDefault().post(new FavoritesEvent()); - EventBus.getDefault().post(FeedItemEvent.updated(item)); - }); - } - - public static Future<?> removeFavoriteItem(final FeedItem item) { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); - adapter.removeFavoriteItem(item); - adapter.close(); - item.removeTag(FeedItem.TAG_FAVORITE); - EventBus.getDefault().post(new FavoritesEvent()); - EventBus.getDefault().post(FeedItemEvent.updated(item)); - }); - } - - /** - * Moves the specified item to the top of the queue. - * - * @param itemId The item to move to the top of the queue - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - */ - public static Future<?> moveQueueItemToTop(final long itemId, final boolean broadcastUpdate) { - return runOnDbThread(() -> { - LongList queueIdList = DBReader.getQueueIDList(); - int index = queueIdList.indexOf(itemId); - if (index >= 0) { - moveQueueItemHelper(index, 0, broadcastUpdate); - } else { - Log.e(TAG, "moveQueueItemToTop: item not found"); - } - }); - } - - /** - * Moves the specified item to the bottom of the queue. - * - * @param itemId The item to move to the bottom of the queue - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - */ - public static Future<?> moveQueueItemToBottom(final long itemId, - final boolean broadcastUpdate) { - return runOnDbThread(() -> { - LongList queueIdList = DBReader.getQueueIDList(); - int index = queueIdList.indexOf(itemId); - if (index >= 0) { - moveQueueItemHelper(index, queueIdList.size() - 1, - broadcastUpdate); - } else { - Log.e(TAG, "moveQueueItemToBottom: item not found"); - } - }); - } - - /** - * Changes the position of a FeedItem in the queue. - * - * @param from Source index. Must be in range 0..queue.size()-1. - * @param to Destination index. Must be in range 0..queue.size()-1. - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. - * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) - */ - public static Future<?> moveQueueItem(final int from, - final int to, final boolean broadcastUpdate) { - return runOnDbThread(() -> moveQueueItemHelper(from, to, broadcastUpdate)); - } - - /** - * Changes the position of a FeedItem in the queue. - * <p/> - * This function must be run using the ExecutorService (dbExec). - * - * @param from Source index. Must be in range 0..queue.size()-1. - * @param to Destination index. Must be in range 0..queue.size()-1. - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. - * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) - */ - private static void moveQueueItemHelper(final int from, - final int to, final boolean broadcastUpdate) { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(adapter); - - if (queue != null) { - if (from >= 0 && from < queue.size() && to >= 0 && to < queue.size()) { - final FeedItem item = queue.remove(from); - queue.add(to, item); - - adapter.setQueue(queue); - if (broadcastUpdate) { - EventBus.getDefault().post(QueueEvent.moved(item, to)); - } - } - } else { - Log.e(TAG, "moveQueueItemHelper: Could not load queue"); - } - adapter.close(); - } - - public static Future<?> resetPagedFeedPage(Feed feed) { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.resetPagedFeedPage(feed); - adapter.close(); - }); - } - - /* - * Sets the 'read'-attribute of all specified FeedItems - * - * @param played New value of the 'read'-attribute, one of FeedItem.PLAYED, FeedItem.NEW, - * FeedItem.UNPLAYED - * @param itemIds IDs of the FeedItems. - */ - public static Future<?> markItemPlayed(final int played, final long... itemIds) { - return markItemPlayed(played, true, itemIds); - } - - /* - * Sets the 'read'-attribute of all specified FeedItems - * - * @param played New value of the 'read'-attribute, one of FeedItem.PLAYED, FeedItem.NEW, - * FeedItem.UNPLAYED - * @param broadcastUpdate true if this operation should trigger a UnreadItemsUpdate broadcast. - * This option is usually set to true - * @param itemIds IDs of the FeedItems. - */ - public static Future<?> markItemPlayed(final int played, final boolean broadcastUpdate, - final long... itemIds) { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItemRead(played, itemIds); - adapter.close(); - if (broadcastUpdate) { - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - } - }); - } - - /** - * Sets the 'read'-attribute of a FeedItem to the specified value. - * - * @param item The FeedItem object - * @param played New value of the 'read'-attribute one of FeedItem.PLAYED, - * FeedItem.NEW, FeedItem.UNPLAYED - * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object. - */ - @NonNull - public static Future<?> markItemPlayed(FeedItem item, int played, boolean resetMediaPosition) { - long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0; - return markItemPlayed(item.getId(), played, mediaId, resetMediaPosition); - } - - @NonNull - private static Future<?> markItemPlayed(final long itemId, - final int played, - final long mediaId, - final boolean resetMediaPosition) { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItemRead(played, itemId, mediaId, - resetMediaPosition); - adapter.close(); - - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - }); - } - - /** - * Sets the 'read'-attribute of all NEW FeedItems of a specific Feed to UNPLAYED. - * - * @param feedId ID of the Feed. - */ - public static Future<?> removeFeedNewFlag(final long feedId) { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItems(FeedItem.NEW, FeedItem.UNPLAYED, feedId); - adapter.close(); - - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - }); - } - - /** - * Sets the 'read'-attribute of all NEW FeedItems to UNPLAYED. - */ - public static Future<?> removeAllNewFlags() { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItems(FeedItem.NEW, FeedItem.UNPLAYED); - adapter.close(); - - EventBus.getDefault().post(new UnreadItemsUpdateEvent()); - }); - } - - static Future<?> addNewFeed(final Context context, final Feed... feeds) { - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feeds); - adapter.close(); - - for (Feed feed : feeds) { - if (!feed.isLocalFeed()) { - SynchronizationQueueSink.enqueueFeedAddedIfSynchronizationIsActive(context, feed.getDownloadUrl()); - } - } - - BackupManager backupManager = new BackupManager(context); - backupManager.dataChanged(); - }); - } - - static Future<?> setCompleteFeed(final Feed... feeds) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setCompleteFeed(feeds); - adapter.close(); - }); - } - - public static Future<?> setItemList(final List<FeedItem> items) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.storeFeedItemlist(items); - adapter.close(); - EventBus.getDefault().post(FeedItemEvent.updated(items)); - }); - } - - /** - * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The - * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved. - * - * @param media The FeedMedia object. - */ - public static Future<?> setFeedMedia(final FeedMedia media) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setMedia(media); - adapter.close(); - }); - } - - /** - * Saves the 'position', 'duration' and 'last played time' attributes of a FeedMedia object - * - * @param media The FeedMedia object. - */ - public static Future<?> setFeedMediaPlaybackInformation(final FeedMedia media) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedMediaPlaybackInformation(media); - adapter.close(); - }); - } - - /** - * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including - * the content of FeedComponent-attributes. - * - * @param item The FeedItem object. - */ - public static Future<?> setFeedItem(final FeedItem item) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setSingleFeedItem(item); - adapter.close(); - EventBus.getDefault().post(FeedItemEvent.updated(item)); - }); - } - - /** - * Updates download URL of a feed - */ - public static Future<?> updateFeedDownloadURL(final String original, final String updated) { - Log.d(TAG, "updateFeedDownloadURL(original: " + original + ", updated: " + updated + ")"); - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedDownloadUrl(original, updated); - adapter.close(); - }); - } - - /** - * Saves a FeedPreferences object in the database. The Feed ID of the FeedPreferences-object MUST NOT be 0. - * - * @param preferences The FeedPreferences object. - */ - public static Future<?> setFeedPreferences(final FeedPreferences preferences) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedPreferences(preferences); - adapter.close(); - EventBus.getDefault().post(new FeedListUpdateEvent(preferences.getFeedID())); - }); - } - - private static boolean itemListContains(List<FeedItem> items, long itemId) { - return indexInItemList(items, itemId) >= 0; - } - - private static int indexInItemList(List<FeedItem> items, long itemId) { - for (int i = 0; i < items.size(); i++) { - FeedItem item = items.get(i); - if (item.getId() == itemId) { - return i; - } - } - return -1; - } - - /** - * Saves if a feed's last update failed - * - * @param lastUpdateFailed true if last update failed - */ - public static Future<?> setFeedLastUpdateFailed(final long feedId, - final boolean lastUpdateFailed) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed); - adapter.close(); - EventBus.getDefault().post(new FeedListUpdateEvent(feedId)); - }); - } - - public static Future<?> setFeedCustomTitle(Feed feed) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedCustomTitle(feed.getId(), feed.getCustomTitle()); - adapter.close(); - EventBus.getDefault().post(new FeedListUpdateEvent(feed)); - }); - } - - /** - * Sort the FeedItems in the queue with the given the named sort order. - * - * @param broadcastUpdate <code>true</code> if this operation should trigger a - * QueueUpdateBroadcast. This option should be set to <code>false</code> - * if the caller wants to avoid unexpected updates of the GUI. - */ - public static Future<?> reorderQueue(@Nullable SortOrder sortOrder, final boolean broadcastUpdate) { - if (sortOrder == null) { - Log.w(TAG, "reorderQueue() - sortOrder is null. Do nothing."); - return runOnDbThread(() -> { }); - } - final Permutor<FeedItem> permutor = FeedItemPermutors.getPermutor(sortOrder); - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(adapter); - - if (queue != null) { - permutor.reorder(queue); - adapter.setQueue(queue); - if (broadcastUpdate) { - EventBus.getDefault().post(QueueEvent.sorted(queue)); - } - } else { - Log.e(TAG, "reorderQueue: Could not load queue"); - } - adapter.close(); - }); - } - - /** - * Set filter of the feed - * - * @param feedId The feed's ID - * @param filterValues Values that represent properties to filter by - */ - public static Future<?> setFeedItemsFilter(final long feedId, - final Set<String> filterValues) { - Log.d(TAG, "setFeedItemsFilter() called with: " + "feedId = [" + feedId + "], filterValues = [" + filterValues + "]"); - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItemFilter(feedId, filterValues); - adapter.close(); - EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId)); - }); - } - - /** - * Set item sort order of the feed - * - */ - public static Future<?> setFeedItemSortOrder(long feedId, @Nullable SortOrder sortOrder) { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.setFeedItemSortOrder(feedId, sortOrder); - adapter.close(); - EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.SORT_ORDER_CHANGED, feedId)); - }); - } - - /** - * Reset the statistics in DB - */ - @NonNull - public static Future<?> resetStatistics() { - return runOnDbThread(() -> { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - adapter.resetAllMediaPlayedDuration(); - adapter.close(); - }); - } - - /** - * Removes the feed with the given download url. This method should NOT be executed on the GUI thread. - * - * @param context Used for accessing the db - * @param downloadUrl URL of the feed. - */ - public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) { - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - Cursor cursor = adapter.getFeedCursorDownloadUrls(); - long feedId = 0; - if (cursor.moveToFirst()) { - do { - if (cursor.getString(1).equals(downloadUrl)) { - feedId = cursor.getLong(0); - } - } while (cursor.moveToNext()); - } - cursor.close(); - adapter.close(); - - if (feedId != 0) { - try { - deleteFeed(context, feedId).get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } else { - Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl); - } - } - - /** - * Submit to the DB thread only if caller is not already on the DB thread. Otherwise, - * just execute synchronously - */ - private static Future<?> runOnDbThread(Runnable runnable) { - if ("DatabaseExecutor".equals(Thread.currentThread().getName())) { - runnable.run(); - return Futures.immediateFuture(null); - } else { - return dbExec.submit(runnable); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java index 86c27ffdc..2fc2ef902 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithm.java @@ -16,6 +16,7 @@ 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; /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedDatabaseWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedDatabaseWriter.java deleted file mode 100644 index 7c21bd7c9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedDatabaseWriter.java +++ /dev/null @@ -1,262 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import android.content.Context; -import android.text.TextUtils; -import android.util.Log; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; -import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.event.FeedListUpdateEvent; -import de.danoeh.antennapod.model.download.DownloadError; -import de.danoeh.antennapod.model.download.DownloadResult; -import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedPreferences; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.storage.database.PodDBAdapter; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import org.greenrobot.eventbus.EventBus; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutionException; - -/** - * Creates and updates feeds in the database. - */ -public abstract class FeedDatabaseWriter { - private static final String TAG = "FeedDbWriter"; - - private static Feed searchFeedByIdentifyingValueOrID(Feed feed) { - if (feed.getId() != 0) { - return DBReader.getFeed(feed.getId()); - } else { - List<Feed> feeds = DBReader.getFeedList(); - for (Feed f : feeds) { - if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) { - f.setItems(DBReader.getFeedItemList(f)); - return f; - } - } - } - return null; - } - - /** - * Get a FeedItem by its identifying value. - */ - private static FeedItem searchFeedItemByIdentifyingValue(List<FeedItem> items, FeedItem searchItem) { - for (FeedItem item : items) { - if (TextUtils.equals(item.getIdentifyingValue(), searchItem.getIdentifyingValue())) { - return item; - } - } - return null; - } - - /** - * Guess if one of the items could actually mean the searched item, even if it uses another identifying value. - * This is to work around podcasters breaking their GUIDs. - */ - private static FeedItem searchFeedItemGuessDuplicate(List<FeedItem> items, FeedItem searchItem) { - // First, see if it is a well-behaving feed that contains an item with the same identifier - for (FeedItem item : items) { - if (FeedItemDuplicateGuesser.sameAndNotEmpty(item.getItemIdentifier(), searchItem.getItemIdentifier())) { - return item; - } - } - // Not found yet, start more expensive guessing - for (FeedItem item : items) { - if (FeedItemDuplicateGuesser.seemDuplicates(item, searchItem)) { - return item; - } - } - return null; - } - - /** - * Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same - * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed. - * These FeedItems will be marked as unread with the exception of the most recent FeedItem. - * - * @param context Used for accessing the DB. - * @param newFeed The new Feed object. - * @param removeUnlistedItems The item list in the new Feed object is considered to be exhaustive. - * I.e. items are removed from the database if they are not in this item list. - * @return The updated Feed from the database if it already existed, or the new Feed from the parameters otherwise. - */ - public static synchronized Feed updateFeed(Context context, Feed newFeed, boolean removeUnlistedItems) { - Feed resultFeed; - List<FeedItem> unlistedItems = new ArrayList<>(); - List<FeedItem> itemsToAddToQueue = new ArrayList<>(); - - PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - - // Look up feed in the feedslist - final Feed savedFeed = searchFeedByIdentifyingValueOrID(newFeed); - if (savedFeed == null) { - Log.d(TAG, "Found no existing Feed with title " - + newFeed.getTitle() + ". Adding as new one."); - - resultFeed = newFeed; - } else { - Log.d(TAG, "Feed with title " + newFeed.getTitle() - + " already exists. Syncing new with existing one."); - - Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); - - if (newFeed.getPageNr() == savedFeed.getPageNr()) { - savedFeed.updateFromOther(newFeed); - savedFeed.getPreferences().updateFromOther(newFeed.getPreferences()); - } else { - Log.d(TAG, "New feed has a higher page number."); - savedFeed.setNextPageLink(newFeed.getNextPageLink()); - } - - // get the most recent date now, before we start changing the list - FeedItem priorMostRecent = savedFeed.getMostRecentItem(); - Date priorMostRecentDate = new Date(); - if (priorMostRecent != null) { - priorMostRecentDate = priorMostRecent.getPubDate(); - } - - // Look for new or updated Items - for (int idx = 0; idx < newFeed.getItems().size(); idx++) { - final FeedItem item = newFeed.getItems().get(idx); - - FeedItem possibleDuplicate = searchFeedItemGuessDuplicate(newFeed.getItems(), item); - if (!newFeed.isLocalFeed() && possibleDuplicate != null && item != possibleDuplicate) { - // Canonical episode is the first one returned (usually oldest) - DBWriter.addDownloadStatus(new DownloadResult(item.getTitle(), - savedFeed.getId(), Feed.FEEDFILETYPE_FEED, false, - DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, - "The podcast host appears to have added the same episode twice. " - + "AntennaPod still refreshed the feed and attempted to repair it." - + "\n\nOriginal episode:\n" + duplicateEpisodeDetails(item) - + "\n\nSecond episode that is also in the feed:\n" - + duplicateEpisodeDetails(possibleDuplicate))); - continue; - } - - FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed.getItems(), item); - if (!newFeed.isLocalFeed() && oldItem == null) { - oldItem = searchFeedItemGuessDuplicate(savedFeed.getItems(), item); - if (oldItem != null) { - Log.d(TAG, "Repaired duplicate: " + oldItem + ", " + item); - DBWriter.addDownloadStatus(new DownloadResult(item.getTitle(), - savedFeed.getId(), Feed.FEEDFILETYPE_FEED, false, - DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE, - "The podcast host changed the ID of an existing episode instead of just " - + "updating the episode itself. AntennaPod still refreshed the feed and " - + "attempted to repair it." - + "\n\nOriginal episode:\n" + duplicateEpisodeDetails(oldItem) - + "\n\nNow the feed contains:\n" + duplicateEpisodeDetails(item))); - oldItem.setItemIdentifier(item.getItemIdentifier()); - - if (oldItem.isPlayed() && oldItem.getMedia() != null) { - EpisodeAction action = new EpisodeAction.Builder(oldItem, EpisodeAction.PLAY) - .currentTimestamp() - .started(oldItem.getMedia().getDuration() / 1000) - .position(oldItem.getMedia().getDuration() / 1000) - .total(oldItem.getMedia().getDuration() / 1000) - .build(); - SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action); - } - } - } - - if (oldItem != null) { - oldItem.updateFromOther(item); - } else { - Log.d(TAG, "Found new item: " + item.getTitle()); - item.setFeed(savedFeed); - - if (idx >= savedFeed.getItems().size()) { - savedFeed.getItems().add(item); - } else { - savedFeed.getItems().add(idx, item); - } - - if (item.getPubDate() == null - || priorMostRecentDate == null - || priorMostRecentDate.before(item.getPubDate()) - || priorMostRecentDate.equals(item.getPubDate())) { - Log.d(TAG, "Performing new episode action for item published on " + item.getPubDate() - + ", prior most recent date = " + priorMostRecentDate); - FeedPreferences.NewEpisodesAction action = savedFeed.getPreferences().getNewEpisodesAction(); - if (action == FeedPreferences.NewEpisodesAction.GLOBAL) { - action = UserPreferences.getNewEpisodesAction(); - } - switch (action) { - case ADD_TO_INBOX: - item.setNew(); - break; - case ADD_TO_QUEUE: - itemsToAddToQueue.add(item); - break; - default: - break; - } - } - } - } - - // identify items to be removed - if (removeUnlistedItems) { - Iterator<FeedItem> it = savedFeed.getItems().iterator(); - while (it.hasNext()) { - FeedItem feedItem = it.next(); - if (searchFeedItemByIdentifyingValue(newFeed.getItems(), feedItem) == null) { - unlistedItems.add(feedItem); - it.remove(); - } - } - } - - // update attributes - savedFeed.setLastModified(newFeed.getLastModified()); - savedFeed.setType(newFeed.getType()); - savedFeed.setLastUpdateFailed(false); - - resultFeed = savedFeed; - } - - try { - if (savedFeed == null) { - DBWriter.addNewFeed(context, newFeed).get(); - // Update with default values that are set in database - resultFeed = searchFeedByIdentifyingValueOrID(newFeed); - } else { - DBWriter.setCompleteFeed(savedFeed).get(); - } - if (removeUnlistedItems) { - DBWriter.deleteFeedItems(context, unlistedItems).get(); - } - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - - // We need to add to queue after items are saved to database - DBWriter.addQueueItem(context, itemsToAddToQueue.toArray(new FeedItem[0])); - - adapter.close(); - - if (savedFeed != null) { - EventBus.getDefault().post(new FeedListUpdateEvent(savedFeed)); - } else { - EventBus.getDefault().post(new FeedListUpdateEvent(Collections.emptyList())); - } - - return resultFeed; - } - - private static String duplicateEpisodeDetails(FeedItem item) { - return "Title: " + item.getTitle() - + "\nID: " + item.getItemIdentifier() - + ((item.getMedia() == null) ? "" : "\nURL: " + item.getMedia().getDownloadUrl()); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemDuplicateGuesser.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemDuplicateGuesser.java deleted file mode 100644 index 1eb8b0577..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemDuplicateGuesser.java +++ /dev/null @@ -1,83 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import android.text.TextUtils; -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; - -import java.text.DateFormat; -import java.util.Locale; - -/** - * Publishers sometimes mess up their feed by adding episodes twice or by changing the ID of existing episodes. - * This class tries to guess if publishers actually meant another episode, - * even if their feed explicitly says that the episodes are different. - */ -public class FeedItemDuplicateGuesser { - public static boolean seemDuplicates(FeedItem item1, FeedItem item2) { - if (sameAndNotEmpty(item1.getItemIdentifier(), item2.getItemIdentifier())) { - return true; - } - FeedMedia media1 = item1.getMedia(); - FeedMedia media2 = item2.getMedia(); - if (media1 == null || media2 == null) { - return false; - } - if (sameAndNotEmpty(media1.getStreamUrl(), media2.getStreamUrl())) { - return true; - } - return titlesLookSimilar(item1, item2) - && datesLookSimilar(item1, item2) - && durationsLookSimilar(media1, media2) - && mimeTypeLooksSimilar(media1, media2); - } - - public static boolean sameAndNotEmpty(String string1, String string2) { - if (TextUtils.isEmpty(string1) || TextUtils.isEmpty(string2)) { - return false; - } - return string1.equals(string2); - } - - private static boolean datesLookSimilar(FeedItem item1, FeedItem item2) { - if (item1.getPubDate() == null || item2.getPubDate() == null) { - return false; - } - DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US); // MM/DD/YY - String dateOriginal = dateFormat.format(item2.getPubDate()); - String dateNew = dateFormat.format(item1.getPubDate()); - return TextUtils.equals(dateOriginal, dateNew); // Same date; time is ignored. - } - - private static boolean durationsLookSimilar(FeedMedia media1, FeedMedia media2) { - return Math.abs(media1.getDuration() - media2.getDuration()) < 10 * 60L * 1000L; - } - - private static boolean mimeTypeLooksSimilar(FeedMedia media1, FeedMedia media2) { - String mimeType1 = media1.getMimeType(); - String mimeType2 = media2.getMimeType(); - if (mimeType1 == null || mimeType2 == null) { - return true; - } - if (mimeType1.contains("/") && mimeType2.contains("/")) { - mimeType1 = mimeType1.substring(0, mimeType1.indexOf("/")); - mimeType2 = mimeType2.substring(0, mimeType2.indexOf("/")); - } - return TextUtils.equals(mimeType1, mimeType2); - } - - private static boolean titlesLookSimilar(FeedItem item1, FeedItem item2) { - return sameAndNotEmpty(canonicalizeTitle(item1.getTitle()), canonicalizeTitle(item2.getTitle())); - } - - private static String canonicalizeTitle(String title) { - if (title == null) { - return ""; - } - return title - .trim() - .replace('“', '"') - .replace('”', '"') - .replace('„', '"') - .replace('—', '-'); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java b/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java deleted file mode 100644 index 3731c4069..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java +++ /dev/null @@ -1,93 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.List; -import java.util.Random; - -import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.storage.preferences.UserPreferences.EnqueueLocation; -import de.danoeh.antennapod.model.playback.Playable; - -/** - * @see DBWriter#addQueueItem(Context, boolean, long...) it uses the class to determine - * the positions of the {@link FeedItem} in the queue. - */ -class ItemEnqueuePositionCalculator { - - @NonNull - private final EnqueueLocation enqueueLocation; - - public ItemEnqueuePositionCalculator(@NonNull EnqueueLocation enqueueLocation) { - this.enqueueLocation = enqueueLocation; - } - - /** - * Determine the position (0-based) that the item(s) should be inserted to the named queue. - * - * @param curQueue the queue to which the item is to be inserted - * @param currentPlaying the currently playing media - */ - public int calcPosition(@NonNull List<FeedItem> curQueue, @Nullable Playable currentPlaying) { - switch (enqueueLocation) { - case BACK: - return curQueue.size(); - case FRONT: - // Return not necessarily 0, so that when a list of items are downloaded and enqueued - // in succession of calls (e.g., users manually tapping download one by one), - // the items enqueued are kept the same order. - // Simply returning 0 will reverse the order. - return getPositionOfFirstNonDownloadingItem(0, curQueue); - case AFTER_CURRENTLY_PLAYING: - int currentlyPlayingPosition = getCurrentlyPlayingPosition(curQueue, currentPlaying); - return getPositionOfFirstNonDownloadingItem( - currentlyPlayingPosition + 1, curQueue); - case RANDOM: - Random random = new Random(); - return random.nextInt(curQueue.size() + 1); - default: - throw new AssertionError("calcPosition() : unrecognized enqueueLocation option: " + enqueueLocation); - } - } - - private int getPositionOfFirstNonDownloadingItem(int startPosition, List<FeedItem> curQueue) { - final int curQueueSize = curQueue.size(); - for (int i = startPosition; i < curQueueSize; i++) { - if (!isItemAtPositionDownloading(i, curQueue)) { - return i; - } // else continue to search; - } - return curQueueSize; - } - - private boolean isItemAtPositionDownloading(int position, List<FeedItem> curQueue) { - FeedItem curItem; - try { - curItem = curQueue.get(position); - } catch (IndexOutOfBoundsException e) { - curItem = null; - } - return curItem != null - && curItem.getMedia() != null - && DownloadServiceInterface.get().isDownloadingEpisode(curItem.getMedia().getDownloadUrl()); - } - - private static int getCurrentlyPlayingPosition(@NonNull List<FeedItem> curQueue, - @Nullable Playable currentPlaying) { - if (!(currentPlaying instanceof FeedMedia)) { - return -1; - } - final long curPlayingItemId = ((FeedMedia) currentPlaying).getItem().getId(); - for (int i = 0; i < curQueue.size(); i++) { - if (curPlayingItemId == curQueue.get(i).getId()) { - return i; - } - } - return -1; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java b/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java deleted file mode 100644 index f616b9c4e..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.danoeh.antennapod.core.sync; - -import android.util.Log; - -import androidx.collection.ArrayMap; -import androidx.core.util.Pair; - -import java.util.List; -import java.util.Map; - -import de.danoeh.antennapod.net.sync.model.EpisodeAction; - -public class EpisodeActionFilter { - - public static final String TAG = "EpisodeActionFilter"; - - public static Map<Pair<String, String>, EpisodeAction> getRemoteActionsOverridingLocalActions( - List<EpisodeAction> remoteActions, - List<EpisodeAction> queuedEpisodeActions) { - // make sure more recent local actions are not overwritten by older remote actions - Map<Pair<String, String>, EpisodeAction> remoteActionsThatOverrideLocalActions = new ArrayMap<>(); - Map<Pair<String, String>, EpisodeAction> localMostRecentPlayActions = - createUniqueLocalMostRecentPlayActions(queuedEpisodeActions); - for (EpisodeAction remoteAction : remoteActions) { - Pair<String, String> key = new Pair<>(remoteAction.getPodcast(), remoteAction.getEpisode()); - switch (remoteAction.getAction()) { - case NEW: - case DOWNLOAD: - break; - case PLAY: - EpisodeAction localMostRecent = localMostRecentPlayActions.get(key); - if (secondActionOverridesFirstAction(remoteAction, localMostRecent)) { - break; - } - EpisodeAction remoteMostRecentAction = remoteActionsThatOverrideLocalActions.get(key); - if (secondActionOverridesFirstAction(remoteAction, remoteMostRecentAction)) { - break; - } - remoteActionsThatOverrideLocalActions.put(key, remoteAction); - break; - case DELETE: - // NEVER EVER call DBWriter.deleteFeedMediaOfItem() here, leads to an infinite loop - break; - default: - Log.e(TAG, "Unknown remoteAction: " + remoteAction); - break; - } - } - - return remoteActionsThatOverrideLocalActions; - } - - private static Map<Pair<String, String>, EpisodeAction> createUniqueLocalMostRecentPlayActions( - List<EpisodeAction> queuedEpisodeActions) { - Map<Pair<String, String>, EpisodeAction> localMostRecentPlayAction; - localMostRecentPlayAction = new ArrayMap<>(); - for (EpisodeAction action : queuedEpisodeActions) { - Pair<String, String> key = new Pair<>(action.getPodcast(), action.getEpisode()); - EpisodeAction mostRecent = localMostRecentPlayAction.get(key); - if (mostRecent == null || mostRecent.getTimestamp() == null) { - localMostRecentPlayAction.put(key, action); - } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { - localMostRecentPlayAction.put(key, action); - } - } - return localMostRecentPlayAction; - } - - private static boolean secondActionOverridesFirstAction(EpisodeAction firstAction, - EpisodeAction secondAction) { - return secondAction != null - && secondAction.getTimestamp() != null - && (firstAction.getTimestamp() == null - || secondAction.getTimestamp().after(firstAction.getTimestamp())); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java b/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java deleted file mode 100644 index 74e5d5cdf..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.danoeh.antennapod.core.sync; - -public class GuidValidator { - - public static boolean isValidGuid(String guid) { - return guid != null - && !guid.trim().isEmpty() - && !guid.equals("null"); - } -} - diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java b/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java deleted file mode 100644 index e7dbbbd3c..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java +++ /dev/null @@ -1,35 +0,0 @@ -package de.danoeh.antennapod.core.sync; - -import java.util.concurrent.locks.ReentrantLock; - -import io.reactivex.Completable; -import io.reactivex.schedulers.Schedulers; - -public class LockingAsyncExecutor { - - static final ReentrantLock lock = new ReentrantLock(); - - /** - * Take the lock and execute runnable (to prevent changes to preferences being lost when enqueueing while sync is - * in progress). If the lock is free, the runnable is directly executed in the calling thread to prevent overhead. - */ - public static void executeLockedAsync(Runnable runnable) { - if (lock.tryLock()) { - try { - runnable.run(); - } finally { - lock.unlock(); - } - } else { - Completable.fromRunnable(() -> { - lock.lock(); - try { - runnable.run(); - } finally { - lock.unlock(); - } - }).subscribeOn(Schedulers.io()) - .subscribe(); - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java deleted file mode 100644 index 30178edf9..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ /dev/null @@ -1,386 +0,0 @@ -package de.danoeh.antennapod.core.sync; - -import android.Manifest; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Build; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.core.app.NotificationCompat; -import androidx.core.content.ContextCompat; -import androidx.core.util.Pair; -import androidx.work.BackoffPolicy; -import androidx.work.Constraints; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; -import de.danoeh.antennapod.event.FeedUpdateRunningEvent; -import de.danoeh.antennapod.event.MessageEvent; -import de.danoeh.antennapod.model.feed.FeedItemFilter; -import de.danoeh.antennapod.model.feed.SortOrder; -import de.danoeh.antennapod.storage.preferences.SynchronizationCredentials; -import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; -import de.danoeh.antennapod.ui.notifications.NotificationUtils; -import org.apache.commons.lang3.StringUtils; -import org.greenrobot.eventbus.EventBus; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.event.SyncServiceEvent; -import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.FeedDatabaseWriter; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueStorage; -import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.storage.database.LongList; -import de.danoeh.antennapod.net.common.UrlChecker; -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.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; -import de.danoeh.antennapod.net.sync.model.EpisodeActionChanges; -import de.danoeh.antennapod.net.sync.model.ISyncService; -import de.danoeh.antennapod.net.sync.model.SubscriptionChanges; -import de.danoeh.antennapod.net.sync.model.SyncServiceException; -import de.danoeh.antennapod.net.sync.model.UploadChangesResponse; -import de.danoeh.antennapod.net.sync.nextcloud.NextcloudSyncService; - -public class SyncService extends Worker { - public static final String TAG = "SyncService"; - private static final String WORK_ID_SYNC = "SyncServiceWorkId"; - - private static boolean isCurrentlyActive = false; - private final SynchronizationQueueStorage synchronizationQueueStorage; - - public SyncService(@NonNull Context context, @NonNull WorkerParameters params) { - super(context, params); - synchronizationQueueStorage = new SynchronizationQueueStorage(context); - } - - @Override - @NonNull - public Result doWork() { - ISyncService activeSyncProvider = getActiveSyncProvider(); - if (activeSyncProvider == null) { - return Result.success(); - } - - SynchronizationSettings.updateLastSynchronizationAttempt(); - setCurrentlyActive(true); - try { - activeSyncProvider.login(); - syncSubscriptions(activeSyncProvider); - waitForDownloadServiceCompleted(); - syncEpisodeActions(activeSyncProvider); - activeSyncProvider.logout(); - clearErrorNotifications(); - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_success)); - SynchronizationSettings.setLastSynchronizationAttemptSuccess(true); - return Result.success(); - } catch (Exception e) { - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_error)); - SynchronizationSettings.setLastSynchronizationAttemptSuccess(false); - Log.e(TAG, Log.getStackTraceString(e)); - - if (e instanceof SyncServiceException) { - if (getRunAttemptCount() % 3 == 2) { - // Do not spam users with notification and retry before notifying - updateErrorNotification(e); - } - return Result.retry(); - } else { - updateErrorNotification(e); - return Result.failure(); - } - } finally { - setCurrentlyActive(false); - } - } - - private static void setCurrentlyActive(boolean active) { - isCurrentlyActive = active; - } - - public static void sync(Context context) { - OneTimeWorkRequest workRequest = getWorkRequest().build(); - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest); - } - - public static void syncImmediately(Context context) { - OneTimeWorkRequest workRequest = getWorkRequest() - .setInitialDelay(0L, TimeUnit.SECONDS) - .build(); - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest); - } - - public static void fullSync(Context context) { - LockingAsyncExecutor.executeLockedAsync(() -> { - SynchronizationSettings.resetTimestamps(); - OneTimeWorkRequest workRequest = getWorkRequest() - .setInitialDelay(0L, TimeUnit.SECONDS) - .build(); - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest); - }); - } - - private void syncSubscriptions(ISyncService syncServiceImpl) throws SyncServiceException { - final long lastSync = SynchronizationSettings.getLastSubscriptionSynchronizationTimestamp(); - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_subscriptions)); - final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(); - SubscriptionChanges subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync); - long newTimeStamp = subscriptionChanges.getTimestamp(); - - List<String> queuedRemovedFeeds = synchronizationQueueStorage.getQueuedRemovedFeeds(); - List<String> queuedAddedFeeds = synchronizationQueueStorage.getQueuedAddedFeeds(); - - Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); - for (String downloadUrl : subscriptionChanges.getAdded()) { - if (!downloadUrl.startsWith("http")) { // Also matches https - Log.d(TAG, "Skipping url: " + downloadUrl); - continue; - } - if (!UrlChecker.containsUrl(localSubscriptions, downloadUrl) && !queuedRemovedFeeds.contains(downloadUrl)) { - Feed feed = new Feed(downloadUrl, null, "Unknown podcast"); - feed.setItems(Collections.emptyList()); - Feed newFeed = FeedDatabaseWriter.updateFeed(getApplicationContext(), feed, false); - FeedUpdateManager.runOnce(getApplicationContext(), newFeed); - } - } - - // remove subscription if not just subscribed (again) - for (String downloadUrl : subscriptionChanges.getRemoved()) { - if (!queuedAddedFeeds.contains(downloadUrl)) { - DBWriter.removeFeedWithDownloadUrl(getApplicationContext(), downloadUrl); - } - } - - if (lastSync == 0) { - Log.d(TAG, "First sync. Adding all local subscriptions."); - queuedAddedFeeds = localSubscriptions; - queuedAddedFeeds.removeAll(subscriptionChanges.getAdded()); - queuedRemovedFeeds.removeAll(subscriptionChanges.getRemoved()); - } - - if (queuedAddedFeeds.size() > 0 || queuedRemovedFeeds.size() > 0) { - Log.d(TAG, "Added: " + StringUtils.join(queuedAddedFeeds, ", ")); - Log.d(TAG, "Removed: " + StringUtils.join(queuedRemovedFeeds, ", ")); - - LockingAsyncExecutor.lock.lock(); - try { - UploadChangesResponse uploadResponse = syncServiceImpl - .uploadSubscriptionChanges(queuedAddedFeeds, queuedRemovedFeeds); - synchronizationQueueStorage.clearFeedQueues(); - newTimeStamp = uploadResponse.timestamp; - } finally { - LockingAsyncExecutor.lock.unlock(); - } - } - SynchronizationSettings.setLastSubscriptionSynchronizationAttemptTimestamp(newTimeStamp); - } - - private void waitForDownloadServiceCompleted() { - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_wait_for_downloads)); - try { - while (true) { - //noinspection BusyWait - Thread.sleep(1000); - FeedUpdateRunningEvent event = EventBus.getDefault().getStickyEvent(FeedUpdateRunningEvent.class); - if (event == null || !event.isFeedUpdateRunning) { - return; - } - } - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - private void syncEpisodeActions(ISyncService syncServiceImpl) throws SyncServiceException { - final long lastSync = SynchronizationSettings.getLastEpisodeActionSynchronizationTimestamp(); - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_download)); - EpisodeActionChanges getResponse = syncServiceImpl.getEpisodeActionChanges(lastSync); - long newTimeStamp = getResponse.getTimestamp(); - List<EpisodeAction> remoteActions = getResponse.getEpisodeActions(); - processEpisodeActions(remoteActions); - - // upload local actions - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_upload)); - List<EpisodeAction> queuedEpisodeActions = synchronizationQueueStorage.getQueuedEpisodeActions(); - if (lastSync == 0) { - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_upload_played)); - List<FeedItem> readItems = DBReader.getEpisodes(0, Integer.MAX_VALUE, - new FeedItemFilter(FeedItemFilter.PLAYED), SortOrder.DATE_NEW_OLD); - Log.d(TAG, "First sync. Upload state for all " + readItems.size() + " played episodes"); - for (FeedItem item : readItems) { - FeedMedia media = item.getMedia(); - if (media == null) { - continue; - } - EpisodeAction played = new EpisodeAction.Builder(item, EpisodeAction.PLAY) - .currentTimestamp() - .started(media.getDuration() / 1000) - .position(media.getDuration() / 1000) - .total(media.getDuration() / 1000) - .build(); - queuedEpisodeActions.add(played); - } - } - if (queuedEpisodeActions.size() > 0) { - LockingAsyncExecutor.lock.lock(); - try { - Log.d(TAG, "Uploading " + queuedEpisodeActions.size() + " actions: " - + StringUtils.join(queuedEpisodeActions, ", ")); - UploadChangesResponse postResponse = syncServiceImpl.uploadEpisodeActions(queuedEpisodeActions); - newTimeStamp = postResponse.timestamp; - Log.d(TAG, "Upload episode response: " + postResponse); - synchronizationQueueStorage.clearEpisodeActionQueue(); - } finally { - LockingAsyncExecutor.lock.unlock(); - } - } - SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp); - } - - private synchronized void processEpisodeActions(List<EpisodeAction> remoteActions) { - Log.d(TAG, "Processing " + remoteActions.size() + " actions"); - if (remoteActions.size() == 0) { - return; - } - - Map<Pair<String, String>, EpisodeAction> playActionsToUpdate = EpisodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, - synchronizationQueueStorage.getQueuedEpisodeActions()); - LongList queueToBeRemoved = new LongList(); - List<FeedItem> updatedItems = new ArrayList<>(); - for (EpisodeAction action : playActionsToUpdate.values()) { - String guid = GuidValidator.isValidGuid(action.getGuid()) ? action.getGuid() : null; - FeedItem feedItem = DBReader.getFeedItemByGuidOrEpisodeUrl(guid, action.getEpisode()); - if (feedItem == null) { - Log.i(TAG, "Unknown feed item: " + action); - continue; - } - if (feedItem.getMedia() == null) { - Log.i(TAG, "Feed item has no media: " + action); - continue; - } - feedItem.getMedia().setPosition(action.getPosition() * 1000); - if (FeedItemUtil.hasAlmostEnded(feedItem.getMedia())) { - Log.d(TAG, "Marking as played: " + action); - feedItem.setPlayed(true); - feedItem.getMedia().setPosition(0); - queueToBeRemoved.add(feedItem.getId()); - } else { - Log.d(TAG, "Setting position: " + action); - } - updatedItems.add(feedItem); - } - DBWriter.removeQueueItem(getApplicationContext(), false, queueToBeRemoved.toArray()); - DBReader.loadAdditionalFeedItemListData(updatedItems); - DBWriter.setItemList(updatedItems); - } - - private void clearErrorNotifications() { - NotificationManager nm = (NotificationManager) getApplicationContext() - .getSystemService(Context.NOTIFICATION_SERVICE); - nm.cancel(R.id.notification_gpodnet_sync_error); - nm.cancel(R.id.notification_gpodnet_sync_autherror); - } - - private void updateErrorNotification(Exception exception) { - Log.d(TAG, "Posting sync error notification"); - final String description = getApplicationContext().getString(R.string.gpodnetsync_error_descr) - + exception.getMessage(); - - if (!UserPreferences.gpodnetNotificationsEnabled()) { - Log.d(TAG, "Skipping sync error notification because of user setting"); - return; - } - if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent.class)) { - EventBus.getDefault().post(new MessageEvent(description)); - return; - } - - Intent intent = getApplicationContext().getPackageManager().getLaunchIntentForPackage( - getApplicationContext().getPackageName()); - PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), - R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT - | (Build.VERSION.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0)); - Notification notification = new NotificationCompat.Builder(getApplicationContext(), - NotificationUtils.CHANNEL_ID_SYNC_ERROR) - .setContentTitle(getApplicationContext().getString(R.string.gpodnetsync_error_title)) - .setContentText(description) - .setStyle(new NotificationCompat.BigTextStyle().bigText(description)) - .setContentIntent(pendingIntent) - .setSmallIcon(R.drawable.ic_notification_sync_error) - .setAutoCancel(true) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .build(); - NotificationManager nm = (NotificationManager) getApplicationContext() - .getSystemService(Context.NOTIFICATION_SERVICE); - if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) - == PackageManager.PERMISSION_GRANTED) { - nm.notify(R.id.notification_gpodnet_sync_error, notification); - } - } - - private static OneTimeWorkRequest.Builder getWorkRequest() { - Constraints.Builder constraints = new Constraints.Builder(); - if (UserPreferences.isAllowMobileSync()) { - constraints.setRequiredNetworkType(NetworkType.CONNECTED); - } else { - constraints.setRequiredNetworkType(NetworkType.UNMETERED); - } - - OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SyncService.class) - .setConstraints(constraints.build()) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES); - - if (isCurrentlyActive) { - // Debounce: don't start sync again immediately after it was finished. - builder.setInitialDelay(2L, TimeUnit.MINUTES); - } else { - // Give it some time, so other possible actions can be queued. - builder.setInitialDelay(20L, TimeUnit.SECONDS); - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_started)); - } - return builder; - } - - private ISyncService getActiveSyncProvider() { - String selectedSyncProviderKey = SynchronizationSettings.getSelectedSyncProviderKey(); - SynchronizationProviderViewData selectedService = SynchronizationProviderViewData - .fromIdentifier(selectedSyncProviderKey); - if (selectedService == null) { - return null; - } - switch (selectedService) { - case GPODDER_NET: - return new GpodnetService(AntennapodHttpClient.getHttpClient(), - SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getDeviceId(), - SynchronizationCredentials.getUsername(), SynchronizationCredentials.getPassword()); - case NEXTCLOUD_GPODDER: - return new NextcloudSyncService(AntennapodHttpClient.getHttpClient(), - SynchronizationCredentials.getHosturl(), SynchronizationCredentials.getUsername(), - SynchronizationCredentials.getPassword()); - default: - return null; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java deleted file mode 100644 index cba713f60..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java +++ /dev/null @@ -1,47 +0,0 @@ -package de.danoeh.antennapod.core.sync; - -import de.danoeh.antennapod.core.R; - -public enum SynchronizationProviderViewData { - GPODDER_NET( - "GPODDER_NET", - R.string.gpodnet_description, - R.drawable.gpodder_icon - ), - NEXTCLOUD_GPODDER( - "NEXTCLOUD_GPODDER", - R.string.synchronization_summary_nextcloud, - R.drawable.nextcloud_logo - ); - - public static SynchronizationProviderViewData fromIdentifier(String provider) { - for (SynchronizationProviderViewData synchronizationProvider : SynchronizationProviderViewData.values()) { - if (synchronizationProvider.getIdentifier().equals(provider)) { - return synchronizationProvider; - } - } - return null; - } - - private final String identifier; - private final int iconResource; - private final int summaryResource; - - SynchronizationProviderViewData(String identifier, int summaryResource, int iconResource) { - this.identifier = identifier; - this.iconResource = iconResource; - this.summaryResource = summaryResource; - } - - public String getIdentifier() { - return identifier; - } - - public int getIconResource() { - return iconResource; - } - - public int getSummaryResource() { - return summaryResource; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java deleted file mode 100644 index 8d59c9146..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java +++ /dev/null @@ -1,82 +0,0 @@ -package de.danoeh.antennapod.core.sync.queue; - -import android.content.Context; - -import de.danoeh.antennapod.core.sync.LockingAsyncExecutor; -import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; - -public class SynchronizationQueueSink { - // To avoid a dependency loop of every class to SyncService, and from SyncService back to every class. - private static Runnable serviceStarterImpl = () -> { }; - - public static void setServiceStarterImpl(Runnable serviceStarter) { - serviceStarterImpl = serviceStarter; - } - - public static void syncNow() { - serviceStarterImpl.run(); - } - - public static void syncNowIfNotSyncedRecently() { - if (System.currentTimeMillis() - SynchronizationSettings.getLastSyncAttempt() > 1000 * 60 * 10) { - syncNow(); - } - } - - public static void clearQueue(Context context) { - LockingAsyncExecutor.executeLockedAsync(new SynchronizationQueueStorage(context)::clearQueue); - } - - public static void enqueueFeedAddedIfSynchronizationIsActive(Context context, String downloadUrl) { - if (!SynchronizationSettings.isProviderConnected()) { - return; - } - LockingAsyncExecutor.executeLockedAsync(() -> { - new SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl); - syncNow(); - }); - } - - public static void enqueueFeedRemovedIfSynchronizationIsActive(Context context, String downloadUrl) { - if (!SynchronizationSettings.isProviderConnected()) { - return; - } - LockingAsyncExecutor.executeLockedAsync(() -> { - new SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl); - syncNow(); - }); - } - - public static void enqueueEpisodeActionIfSynchronizationIsActive(Context context, EpisodeAction action) { - if (!SynchronizationSettings.isProviderConnected()) { - return; - } - LockingAsyncExecutor.executeLockedAsync(() -> { - new SynchronizationQueueStorage(context).enqueueEpisodeAction(action); - syncNow(); - }); - } - - public static void enqueueEpisodePlayedIfSynchronizationIsActive(Context context, FeedMedia media, - boolean completed) { - if (!SynchronizationSettings.isProviderConnected()) { - return; - } - if (media.getItem() == null || media.getItem().getFeed().isLocalFeed()) { - return; - } - if (media.getStartPosition() < 0 || (!completed && media.getStartPosition() >= media.getPosition())) { - return; - } - EpisodeAction action = new EpisodeAction.Builder(media.getItem(), EpisodeAction.PLAY) - .currentTimestamp() - .started(media.getStartPosition() / 1000) - .position((completed ? media.getDuration() : media.getPosition()) / 1000) - .total(media.getDuration() / 1000) - .build(); - enqueueEpisodeActionIfSynchronizationIsActive(context, action); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java b/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java deleted file mode 100644 index 407d69fd6..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java +++ /dev/null @@ -1,158 +0,0 @@ -package de.danoeh.antennapod.core.sync.queue; - -import android.content.Context; -import android.content.SharedPreferences; - -import org.json.JSONArray; -import org.json.JSONException; - -import java.util.ArrayList; - -import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; -import de.danoeh.antennapod.net.sync.model.EpisodeAction; - -public class SynchronizationQueueStorage { - - private static final String NAME = "synchronization"; - private static final String QUEUED_EPISODE_ACTIONS = "sync_queued_episode_actions"; - private static final String QUEUED_FEEDS_REMOVED = "sync_removed"; - private static final String QUEUED_FEEDS_ADDED = "sync_added"; - private final SharedPreferences sharedPreferences; - - public SynchronizationQueueStorage(Context context) { - this.sharedPreferences = context.getSharedPreferences(NAME, Context.MODE_PRIVATE); - } - - public ArrayList<EpisodeAction> getQueuedEpisodeActions() { - ArrayList<EpisodeAction> actions = new ArrayList<>(); - try { - String json = getSharedPreferences() - .getString(QUEUED_EPISODE_ACTIONS, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - actions.add(EpisodeAction.readFromJsonObject(queue.getJSONObject(i))); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return actions; - } - - public ArrayList<String> getQueuedRemovedFeeds() { - ArrayList<String> removedFeedUrls = new ArrayList<>(); - try { - String json = getSharedPreferences() - .getString(QUEUED_FEEDS_REMOVED, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - removedFeedUrls.add(queue.getString(i)); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return removedFeedUrls; - - } - - public ArrayList<String> getQueuedAddedFeeds() { - ArrayList<String> addedFeedUrls = new ArrayList<>(); - try { - String json = getSharedPreferences() - .getString(QUEUED_FEEDS_ADDED, "[]"); - JSONArray queue = new JSONArray(json); - for (int i = 0; i < queue.length(); i++) { - addedFeedUrls.add(queue.getString(i)); - } - } catch (JSONException e) { - e.printStackTrace(); - } - return addedFeedUrls; - } - - public void clearEpisodeActionQueue() { - getSharedPreferences().edit() - .putString(QUEUED_EPISODE_ACTIONS, "[]").apply(); - - } - - public void clearFeedQueues() { - getSharedPreferences().edit() - .putString(QUEUED_FEEDS_ADDED, "[]") - .putString(QUEUED_FEEDS_REMOVED, "[]") - .apply(); - } - - protected void clearQueue() { - SynchronizationSettings.resetTimestamps(); - getSharedPreferences().edit() - .putString(QUEUED_EPISODE_ACTIONS, "[]") - .putString(QUEUED_FEEDS_ADDED, "[]") - .putString(QUEUED_FEEDS_REMOVED, "[]") - .apply(); - - } - - protected void enqueueFeedAdded(String downloadUrl) { - SharedPreferences sharedPreferences = getSharedPreferences(); - try { - JSONArray addedQueue = new JSONArray(sharedPreferences.getString(QUEUED_FEEDS_ADDED, "[]")); - addedQueue.put(downloadUrl); - JSONArray removedQueue = new JSONArray(sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]")); - removedQueue.remove(indexOf(downloadUrl, removedQueue)); - sharedPreferences.edit() - .putString(QUEUED_FEEDS_ADDED, addedQueue.toString()) - .putString(QUEUED_FEEDS_REMOVED, removedQueue.toString()) - .apply(); - - } catch (JSONException jsonException) { - jsonException.printStackTrace(); - } - } - - protected void enqueueFeedRemoved(String downloadUrl) { - SharedPreferences sharedPreferences = getSharedPreferences(); - try { - JSONArray removedQueue = new JSONArray(sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]")); - removedQueue.put(downloadUrl); - JSONArray addedQueue = new JSONArray(sharedPreferences.getString(QUEUED_FEEDS_ADDED, "[]")); - addedQueue.remove(indexOf(downloadUrl, addedQueue)); - sharedPreferences.edit() - .putString(QUEUED_FEEDS_ADDED, addedQueue.toString()) - .putString(QUEUED_FEEDS_REMOVED, removedQueue.toString()) - .apply(); - } catch (JSONException jsonException) { - jsonException.printStackTrace(); - } - } - - private int indexOf(String string, JSONArray array) { - try { - for (int i = 0; i < array.length(); i++) { - if (array.getString(i).equals(string)) { - return i; - } - } - } catch (JSONException jsonException) { - jsonException.printStackTrace(); - } - return -1; - } - - protected void enqueueEpisodeAction(EpisodeAction action) { - SharedPreferences sharedPreferences = getSharedPreferences(); - String json = sharedPreferences.getString(QUEUED_EPISODE_ACTIONS, "[]"); - try { - JSONArray queue = new JSONArray(json); - queue.put(action.writeToJsonObject()); - sharedPreferences.edit().putString( - QUEUED_EPISODE_ACTIONS, queue.toString() - ).apply(); - } catch (JSONException jsonException) { - jsonException.printStackTrace(); - } - } - - private SharedPreferences getSharedPreferences() { - return sharedPreferences; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java index f72589a38..db8a33576 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java @@ -2,8 +2,6 @@ package de.danoeh.antennapod.core.util; import androidx.annotation.NonNull; -import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.storage.preferences.UserPreferences; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; @@ -68,9 +66,4 @@ public class FeedItemUtil { } return null; } - - public static boolean hasAlmostEnded(FeedMedia media) { - int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); - return media.getDuration() > 0 && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000; - } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java deleted file mode 100644 index b16f0f1aa..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.danoeh.antennapod.core.util.comparator; - -import java.util.Comparator; - -import de.danoeh.antennapod.model.feed.FeedItem; - -/** - * Compares the pubDate of two FeedItems for sorting. - */ -public class FeedItemPubdateComparator implements Comparator<FeedItem> { - - /** - * Returns a new instance of this comparator in reverse order. - */ - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - if (rhs.getPubDate() == null && lhs.getPubDate() == null) { - return 0; - } else if (rhs.getPubDate() == null) { - return 1; - } else if (lhs.getPubDate() == null) { - return -1; - } - return rhs.getPubDate().compareTo(lhs.getPubDate()); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java index 89097d9ee..17077c237 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java @@ -19,12 +19,13 @@ import de.danoeh.antennapod.core.service.FeedUpdateWorker; import de.danoeh.antennapod.net.common.NetworkUtils; import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.model.feed.Feed; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.storage.preferences.UserPreferences; import org.greenrobot.eventbus.EventBus; import java.util.concurrent.TimeUnit; -public class FeedUpdateManager { +public class FeedUpdateManagerImpl extends FeedUpdateManager { public static final String WORK_TAG_FEED_UPDATE = "feedUpdate"; private static final String WORK_ID_FEED_UPDATE = "de.danoeh.antennapod.core.service.FeedUpdateWorker"; private static final String WORK_ID_FEED_UPDATE_MANUAL = "feedUpdateManual"; @@ -33,15 +34,11 @@ public class FeedUpdateManager { public static final String EXTRA_EVEN_ON_MOBILE = "even_on_mobile"; private static final String TAG = "AutoUpdateManager"; - private FeedUpdateManager() { - - } - /** * Start / restart periodic auto feed refresh * @param context Context */ - public static void restartUpdateAlarm(Context context, boolean replace) { + public void restartUpdateAlarm(Context context, boolean replace) { if (UserPreferences.isAutoUpdateDisabled()) { WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE); } else { @@ -56,15 +53,15 @@ public class FeedUpdateManager { } } - public static void runOnce(Context context) { + public void runOnce(Context context) { runOnce(context, null, false); } - public static void runOnce(Context context, Feed feed) { + public void runOnce(Context context, Feed feed) { runOnce(context, feed, false); } - public static void runOnce(Context context, Feed feed, boolean nextPage) { + public void runOnce(Context context, Feed feed, boolean nextPage) { OneTimeWorkRequest.Builder workRequest = new OneTimeWorkRequest.Builder(FeedUpdateWorker.class) .setInitialDelay(0L, TimeUnit.MILLISECONDS) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) @@ -84,11 +81,11 @@ public class FeedUpdateManager { ExistingWorkPolicy.REPLACE, workRequest.build()); } - public static void runOnceOrAsk(@NonNull Context context) { + public void runOnceOrAsk(@NonNull Context context) { runOnceOrAsk(context, null); } - public static void runOnceOrAsk(@NonNull Context context, @Nullable Feed feed) { + public void runOnceOrAsk(@NonNull Context context, @Nullable Feed feed) { Log.d(TAG, "Run auto update immediately in background."); if (feed != null && feed.isLocalFeed()) { runOnce(context, feed); @@ -101,7 +98,7 @@ public class FeedUpdateManager { } } - private static void confirmMobileRefresh(final Context context, @Nullable Feed feed) { + private void confirmMobileRefresh(final Context context, @Nullable Feed feed) { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) .setTitle(R.string.feed_refresh_title) .setPositiveButton(R.string.confirm_mobile_streaming_button_once, diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/MediaSizeLoader.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/MediaSizeLoader.java index dcfff0af2..779f3b947 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/MediaSizeLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/MediaSizeLoader.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.util.download; import android.text.TextUtils; import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.net.common.NetworkUtils; import de.danoeh.antennapod.model.feed.FeedMedia; import io.reactivex.Single; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/NetworkConnectionChangeHandler.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/NetworkConnectionChangeHandler.java index cafbde368..79c6e76e1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/NetworkConnectionChangeHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/NetworkConnectionChangeHandler.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.core.util.download; import android.content.Context; import android.util.Log; -import de.danoeh.antennapod.core.storage.AutoDownloadManager; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.net.common.NetworkUtils; @@ -17,7 +17,7 @@ public abstract class NetworkConnectionChangeHandler { public static void networkChangedDetected() { if (NetworkUtils.isAutoDownloadAllowed()) { Log.d(TAG, "auto-dl network available, starting auto-download"); - AutoDownloadManager.autodownloadUndownloadedItems(context); + AutoDownloadManager.getInstance().autodownloadUndownloadedItems(context); } else { // if new network is Wi-Fi, finish ongoing downloads, // otherwise cancel all downloads if (NetworkUtils.isNetworkRestricted()) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java index 90751cbdb..9a622d440 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlayableUtils.java @@ -1,6 +1,6 @@ package de.danoeh.antennapod.core.util.playback; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.playback.Playable; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index b56a0481e..41342cb1f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -14,7 +14,7 @@ import android.view.SurfaceHolder; import androidx.annotation.NonNull; import de.danoeh.antennapod.core.service.playback.PlaybackServiceInterface; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.event.playback.PlaybackPositionEvent; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.event.playback.PlaybackServiceEvent; diff --git a/core/src/main/res/drawable-nodpi/gpodder_icon.png b/core/src/main/res/drawable-nodpi/gpodder_icon.png Binary files differdeleted file mode 100644 index cd133aa98..000000000 --- a/core/src/main/res/drawable-nodpi/gpodder_icon.png +++ /dev/null diff --git a/core/src/main/res/drawable-nodpi/nextcloud_logo.png b/core/src/main/res/drawable-nodpi/nextcloud_logo.png Binary files differdeleted file mode 100644 index 2164e37fb..000000000 --- a/core/src/main/res/drawable-nodpi/nextcloud_logo.png +++ /dev/null diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml index 90d143d38..7bb78c1c9 100644 --- a/core/src/main/res/values/ids.xml +++ b/core/src/main/res/values/ids.xml @@ -16,8 +16,6 @@ <item name="view_type_episode_item" type="id"/> <!-- Notifications need unique IDs to update/cancel them --> - <item name="notification_gpodnet_sync_error" type="id"/> - <item name="notification_gpodnet_sync_autherror" type="id"/> <item name="notification_downloading" type="id"/> <item name="notification_updating_feeds" type="id"/> <item name="notification_download_report" type="id"/> |