From 6f3a9b16764a57e43994ccbeeada5224dee93f44 Mon Sep 17 00:00:00 2001 From: ByteHamster Date: Fri, 29 Mar 2024 17:45:14 +0100 Subject: Create module for sync service and move DBWriter to database module (#7040) --- app/build.gradle | 1 + .../de/test/antennapod/playback/PlaybackTest.java | 2 +- .../danoeh/antennapod/activity/MainActivity.java | 13 +- .../activity/OnlineFeedViewActivity.java | 4 +- .../antennapod/activity/OpmlImportActivity.java | 6 +- .../antennapod/activity/VideoplayerActivity.java | 2 +- .../antennapod/adapter/DownloadLogAdapter.java | 4 +- .../actionbutton/CancelDownloadActionButton.java | 2 +- .../adapter/actionbutton/DeleteActionButton.java | 2 +- .../actionbutton/MarkAsPlayedActionButton.java | 2 +- .../adapter/actionbutton/PlayActionButton.java | 2 +- .../antennapod/dialog/EditUrlSettingsDialog.java | 6 +- .../antennapod/dialog/FeedItemFilterDialog.java | 2 +- .../danoeh/antennapod/dialog/RemoveFeedDialog.java | 2 +- .../danoeh/antennapod/dialog/RenameItemDialog.java | 2 +- .../antennapod/dialog/TagSettingsDialog.java | 2 +- .../antennapod/fragment/AddFeedFragment.java | 6 +- .../fragment/CompletedDownloadsFragment.java | 4 +- .../antennapod/fragment/DownloadLogFragment.java | 2 +- .../antennapod/fragment/EpisodesListFragment.java | 6 +- .../antennapod/fragment/FeedInfoFragment.java | 2 +- .../antennapod/fragment/FeedItemlistFragment.java | 13 +- .../antennapod/fragment/FeedSettingsFragment.java | 6 +- .../danoeh/antennapod/fragment/InboxFragment.java | 2 +- .../antennapod/fragment/NavDrawerFragment.java | 2 +- .../fragment/PlaybackHistoryFragment.java | 2 +- .../danoeh/antennapod/fragment/QueueFragment.java | 8 +- .../antennapod/fragment/SubscriptionFragment.java | 6 +- .../actions/EpisodeMultiSelectActionHandler.java | 2 +- .../actions/FeedMultiSelectActionHandler.java | 2 +- .../preferences/DownloadsPreferencesFragment.java | 4 +- .../swipeactions/AddToQueueSwipeAction.java | 2 +- .../fragment/swipeactions/DeleteSwipeAction.java | 2 +- .../swipeactions/MarkFavoriteSwipeAction.java | 2 +- .../swipeactions/RemoveFromHistorySwipeAction.java | 2 +- .../swipeactions/RemoveFromQueueSwipeAction.java | 2 +- .../menuhandler/FeedItemMenuHandler.java | 13 +- .../antennapod/menuhandler/FeedMenuHandler.java | 2 +- .../receiver/PowerConnectionReceiver.java | 4 +- .../de/danoeh/antennapod/receiver/SPAReceiver.java | 6 +- .../de/danoeh/antennapod/ui/home/HomeFragment.java | 7 +- core/build.gradle | 2 + .../danoeh/antennapod/core/ClientConfigurator.java | 10 +- .../antennapod/core/backup/OpmlBackupAgent.java | 6 +- .../antennapod/core/feed/LocalFeedUpdater.java | 4 +- .../core/receiver/FeedUpdateReceiver.java | 4 +- .../antennapod/core/service/FeedUpdateWorker.java | 16 +- .../download/DownloadServiceInterfaceImpl.java | 2 +- .../service/download/EpisodeDownloadWorker.java | 2 +- .../download/handler/MediaDownloadedHandler.java | 4 +- .../core/service/playback/PlaybackService.java | 17 +- .../core/storage/APCleanupAlgorithm.java | 1 + .../core/storage/APQueueCleanupAlgorithm.java | 1 + .../core/storage/AutoDownloadManager.java | 54 - .../core/storage/AutoDownloadManagerImpl.java | 55 + .../danoeh/antennapod/core/storage/DBWriter.java | 1065 -------------------- .../storage/ExceptFavoriteCleanupAlgorithm.java | 1 + .../core/storage/FeedDatabaseWriter.java | 262 ----- .../core/storage/FeedItemDuplicateGuesser.java | 83 -- .../storage/ItemEnqueuePositionCalculator.java | 93 -- .../antennapod/core/sync/EpisodeActionFilter.java | 77 -- .../danoeh/antennapod/core/sync/GuidValidator.java | 11 - .../antennapod/core/sync/LockingAsyncExecutor.java | 35 - .../danoeh/antennapod/core/sync/SyncService.java | 386 ------- .../core/sync/SynchronizationProviderViewData.java | 47 - .../core/sync/queue/SynchronizationQueueSink.java | 82 -- .../sync/queue/SynchronizationQueueStorage.java | 158 --- .../danoeh/antennapod/core/util/FeedItemUtil.java | 7 - .../util/comparator/FeedItemPubdateComparator.java | 27 - .../core/util/download/FeedUpdateManager.java | 121 --- .../core/util/download/FeedUpdateManagerImpl.java | 118 +++ .../core/util/download/MediaSizeLoader.java | 2 +- .../download/NetworkConnectionChangeHandler.java | 4 +- .../core/util/playback/PlayableUtils.java | 2 +- .../core/util/playback/PlaybackController.java | 2 +- core/src/main/res/drawable-nodpi/gpodder_icon.png | Bin 32098 -> 0 bytes .../src/main/res/drawable-nodpi/nextcloud_logo.png | Bin 3432 -> 0 bytes core/src/main/res/values/ids.xml | 2 - .../antennapod/core/feed/LocalFeedUpdaterTest.java | 2 +- .../antennapod/core/storage/DbCleanupTests.java | 11 +- .../core/storage/DbNullCleanupAlgorithmTest.java | 5 +- .../core/storage/DbQueueCleanupAlgorithmTest.java | 4 +- .../antennapod/core/storage/DbReaderTest.java | 1 + .../antennapod/core/storage/DbTasksTest.java | 2 + .../antennapod/core/storage/DbWriterTest.java | 1 + .../ExceptFavoriteCleanupAlgorithmTest.java | 8 +- .../core/storage/FeedItemDuplicateGuesserTest.java | 1 + .../storage/ItemEnqueuePositionCalculatorTest.java | 1 + .../core/sync/EpisodeActionFilterTest.java | 212 ---- .../antennapod/core/sync/GuidValidatorTest.java | 19 - .../serviceinterface/AutoDownloadManager.java | 39 + .../serviceinterface/FeedUpdateManager.java | 30 + net/sync/service-interface/README.md | 3 + net/sync/service-interface/build.gradle | 19 + .../serviceinterface/LockingAsyncExecutor.java | 43 + .../SynchronizationProviderViewData.java | 45 + .../serviceinterface/SynchronizationQueueSink.java | 81 ++ .../SynchronizationQueueStorage.java | 158 +++ .../src/main/res/drawable-nodpi/gpodder_icon.png | Bin 0 -> 32098 bytes .../src/main/res/drawable-nodpi/nextcloud_logo.png | Bin 0 -> 3432 bytes .../service-interface/src/main/res/values/ids.xml | 5 + net/sync/service/README.md | 3 + net/sync/service/build.gradle | 36 + .../net/sync/service/EpisodeActionFilter.java | 77 ++ .../antennapod/net/sync/service/GuidValidator.java | 11 + .../antennapod/net/sync/service/SyncService.java | 390 +++++++ .../net/sync/service/EpisodeActionFilterTest.java | 212 ++++ .../net/sync/service/GuidValidatorTest.java | 19 + settings.gradle | 2 + storage/database/build.gradle | 11 + .../antennapod/storage/database/DBWriter.java | 1055 +++++++++++++++++++ .../storage/database/FeedDatabaseWriter.java | 259 +++++ .../storage/database/FeedItemDuplicateGuesser.java | 83 ++ .../database/FeedItemPubdateComparator.java | 27 + .../database/ItemEnqueuePositionCalculator.java | 94 ++ storage/importexport/build.gradle | 2 + .../src/main/res/values/pending_intent.xml | 1 - ui/preferences/build.gradle | 2 + .../GpodderAuthenticationFragment.java | 6 +- .../NextcloudAuthenticationFragment.java | 6 +- .../SynchronizationPreferencesFragment.java | 6 +- .../ui/statistics/StatisticsFragment.java | 2 +- ui/widget/build.gradle | 1 + 123 files changed, 3049 insertions(+), 2872 deletions(-) delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManager.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManagerImpl.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/FeedDatabaseWriter.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemDuplicateGuesser.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculator.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/EpisodeActionFilter.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/GuidValidator.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/LockingAsyncExecutor.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/SynchronizationProviderViewData.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueSink.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/sync/queue/SynchronizationQueueStorage.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java delete mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java create mode 100644 core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java delete mode 100644 core/src/main/res/drawable-nodpi/gpodder_icon.png delete mode 100644 core/src/main/res/drawable-nodpi/nextcloud_logo.png delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java delete mode 100644 core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java create mode 100644 net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/AutoDownloadManager.java create mode 100644 net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/FeedUpdateManager.java create mode 100644 net/sync/service-interface/README.md create mode 100644 net/sync/service-interface/build.gradle create mode 100644 net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/LockingAsyncExecutor.java create mode 100644 net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationProviderViewData.java create mode 100644 net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationQueueSink.java create mode 100644 net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationQueueStorage.java create mode 100644 net/sync/service-interface/src/main/res/drawable-nodpi/gpodder_icon.png create mode 100644 net/sync/service-interface/src/main/res/drawable-nodpi/nextcloud_logo.png create mode 100644 net/sync/service-interface/src/main/res/values/ids.xml create mode 100644 net/sync/service/README.md create mode 100644 net/sync/service/build.gradle create mode 100644 net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/EpisodeActionFilter.java create mode 100644 net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/GuidValidator.java create mode 100644 net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/SyncService.java create mode 100644 net/sync/service/src/test/java/de/danoeh/antennapod/net/sync/service/EpisodeActionFilterTest.java create mode 100644 net/sync/service/src/test/java/de/danoeh/antennapod/net/sync/service/GuidValidatorTest.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBWriter.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedDatabaseWriter.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemDuplicateGuesser.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemPubdateComparator.java create mode 100644 storage/database/src/main/java/de/danoeh/antennapod/storage/database/ItemEnqueuePositionCalculator.java diff --git a/app/build.gradle b/app/build.gradle index 1f813d092..84866a9e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -86,6 +86,7 @@ dependencies { implementation project(':ui:widget') implementation project(':ui:preferences') implementation project(':ui:statistics') + implementation project(':net:sync:service-interface') annotationProcessor "androidx.annotation:annotation:$annotationVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion" diff --git a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java index e5fd1ad19..aa1560fe7 100644 --- a/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java +++ b/app/src/androidTest/java/de/test/antennapod/playback/PlaybackTest.java @@ -13,7 +13,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; 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.storage.database.LongList; import de.danoeh.antennapod.core.util.playback.PlaybackController; import de.danoeh.antennapod.model.feed.FeedItem; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java index f7bfbce7b..ee8a80caf 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/MainActivity.java @@ -35,10 +35,11 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.core.util.download.FeedUpdateManagerImpl; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; import de.danoeh.antennapod.ui.common.ThemeSwitcher; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.dialog.rating.RatingDialogManager; import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedUpdateRunningEvent; @@ -166,12 +167,12 @@ public class MainActivity extends CastEnabledActivity { sheetBehavior.setHideable(false); sheetBehavior.setBottomSheetCallback(bottomSheetCallback); - FeedUpdateManager.restartUpdateAlarm(this, false); + FeedUpdateManager.getInstance().restartUpdateAlarm(this, false); SynchronizationQueueSink.syncNowIfNotSyncedRecently(); AutomaticDatabaseExportWorker.enqueueIfNeeded(this, false); WorkManager.getInstance(this) - .getWorkInfosByTagLiveData(FeedUpdateManager.WORK_TAG_FEED_UPDATE) + .getWorkInfosByTagLiveData(FeedUpdateManagerImpl.WORK_TAG_FEED_UPDATE) .observe(this, workInfos -> { boolean isRefreshingFeeds = false; for (WorkInfo workInfo : workInfos) { @@ -301,7 +302,7 @@ public class MainActivity extends CastEnabledActivity { private void checkFirstLaunch() { SharedPreferences prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE); if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) { - FeedUpdateManager.restartUpdateAlarm(this, true); + FeedUpdateManager.getInstance().restartUpdateAlarm(this, true); SharedPreferences.Editor edit = prefs.edit(); edit.putBoolean(PREF_IS_FIRST_LAUNCH, false); @@ -630,7 +631,7 @@ public class MainActivity extends CastEnabledActivity { new DownloadLogFragment().show(getSupportFragmentManager(), null); } if (intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false)) { - FeedUpdateManager.runOnceOrAsk(this); + FeedUpdateManager.getInstance().runOnceOrAsk(this); } // to avoid handling the intent twice when the configuration changes setIntent(new Intent(MainActivity.this, MainActivity.class)); diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index 3a9fc9b46..4ad11588b 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -32,7 +32,7 @@ import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; import de.danoeh.antennapod.ui.common.ThemeSwitcher; import de.danoeh.antennapod.core.service.download.DownloadRequestCreator; import de.danoeh.antennapod.net.discovery.FeedUrlNotFoundException; -import de.danoeh.antennapod.core.storage.FeedDatabaseWriter; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; import de.danoeh.antennapod.core.service.playback.PlaybackServiceInterface; import de.danoeh.antennapod.core.util.DownloadErrorLabel; import de.danoeh.antennapod.databinding.EditTextDialogBinding; @@ -48,7 +48,7 @@ import de.danoeh.antennapod.model.download.DownloadResult; import de.danoeh.antennapod.core.service.download.Downloader; import de.danoeh.antennapod.core.service.download.HttpDownloader; 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.net.discovery.CombinedSearcher; import de.danoeh.antennapod.net.discovery.PodcastSearchResult; import de.danoeh.antennapod.net.discovery.PodcastSearcherRegistry; diff --git a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java index ed11e6910..d78f54b18 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/OpmlImportActivity.java @@ -26,10 +26,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.ui.common.ThemeSwitcher; -import de.danoeh.antennapod.core.storage.FeedDatabaseWriter; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; import de.danoeh.antennapod.databinding.OpmlSelectionBinding; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.storage.importexport.OpmlElement; @@ -104,7 +104,7 @@ public class OpmlImportActivity extends AppCompatActivity { feed.setItems(Collections.emptyList()); FeedDatabaseWriter.updateFeed(this, feed, false); } - FeedUpdateManager.runOnce(this); + FeedUpdateManager.getInstance().runOnce(this); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java index 6e0a84bf6..eb777fda9 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -46,7 +46,7 @@ import de.danoeh.antennapod.fragment.ChaptersFragment; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; 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.ui.common.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index b6df9526c..84c9709bb 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -10,9 +10,9 @@ import android.widget.Toast; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.actionbutton.DownloadActionButton; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.storage.database.DBReader; import de.danoeh.antennapod.core.util.DownloadErrorLabel; -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; @@ -105,7 +105,7 @@ public class DownloadLogAdapter extends BaseAdapter { Log.e(TAG, "Could not find feed for feed id: " + status.getFeedfileId()); return; } - FeedUpdateManager.runOnce(context, feed); + FeedUpdateManager.getInstance().runOnce(context, feed); }); } else if (status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { holder.secondaryActionButton.setOnClickListener(v -> { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java index c9500340b..fa4b19b4d 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/CancelDownloadActionButton.java @@ -9,7 +9,7 @@ import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfa import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.storage.preferences.UserPreferences; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; public class CancelDownloadActionButton extends ItemActionButton { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java index 8ee31a03c..6b9114e81 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/DeleteActionButton.java @@ -10,7 +10,7 @@ import java.util.Collections; import de.danoeh.antennapod.R; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.view.LocalDeleteModal; public class DeleteActionButton extends ItemActionButton { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java index 8dc4ffe33..34fef11dc 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/MarkAsPlayedActionButton.java @@ -7,7 +7,7 @@ import android.view.View; import de.danoeh.antennapod.R; import de.danoeh.antennapod.model.feed.FeedItem; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; public class MarkAsPlayedActionButton extends ItemActionButton { diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java index e0a15191e..5f7f17670 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/actionbutton/PlayActionButton.java @@ -5,7 +5,7 @@ import android.util.Log; import androidx.annotation.DrawableRes; import androidx.annotation.StringRes; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.MessageEvent; import de.danoeh.antennapod.model.feed.FeedItem; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EditUrlSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/EditUrlSettingsDialog.java index 0edb66b64..67c5d85cf 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/EditUrlSettingsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/EditUrlSettingsDialog.java @@ -6,8 +6,8 @@ import android.view.LayoutInflater; import androidx.appcompat.app.AlertDialog; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.databinding.EditTextDialogBinding; import de.danoeh.antennapod.model.feed.Feed; @@ -48,7 +48,7 @@ public abstract class EditUrlSettingsDialog { try { DBWriter.updateFeedDownloadURL(original, updated).get(); feed.setDownloadUrl(updated); - FeedUpdateManager.runOnce(activityRef.get(), feed); + FeedUpdateManager.getInstance().runOnce(activityRef.get(), feed); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java index e91e88fbf..a88a6600a 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/FeedItemFilterDialog.java @@ -1,7 +1,7 @@ package de.danoeh.antennapod.dialog; import android.os.Bundle; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.model.feed.Feed; import java.util.Set; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java index 363b87ca6..ffa374b6f 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/RemoveFeedDialog.java @@ -13,7 +13,7 @@ import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import io.reactivex.Completable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java index fd1403313..bbe6fd16c 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/RenameItemDialog.java @@ -12,7 +12,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import de.danoeh.antennapod.R; import de.danoeh.antennapod.storage.database.NavDrawerData; import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.databinding.EditTextDialogBinding; import de.danoeh.antennapod.model.feed.FeedPreferences; diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java index 4b3ed7db7..8112c0955 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/TagSettingsDialog.java @@ -23,7 +23,7 @@ import java.util.Set; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.SimpleChipAdapter; 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.storage.database.NavDrawerData; import de.danoeh.antennapod.databinding.EditTagsDialogBinding; import de.danoeh.antennapod.model.feed.FeedCounter; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java index 1ec917176..4d1848296 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/AddFeedFragment.java @@ -26,9 +26,9 @@ import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.OpmlImportActivity; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.model.feed.Feed; -import de.danoeh.antennapod.core.storage.FeedDatabaseWriter; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.databinding.AddfeedBinding; import de.danoeh.antennapod.databinding.EditTextDialogBinding; @@ -204,7 +204,7 @@ public class AddFeedFragment extends Fragment { dirFeed.setItems(Collections.emptyList()); dirFeed.setSortOrder(SortOrder.EPISODE_TITLE_A_Z); Feed fromDatabase = FeedDatabaseWriter.updateFeed(getContext(), dirFeed, false); - FeedUpdateManager.runOnce(requireContext(), fromDatabase); + FeedUpdateManager.getInstance().runOnce(requireContext(), fromDatabase); return fromDatabase; } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index 146ac6a9b..9db4f585a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -20,9 +20,9 @@ import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.adapter.actionbutton.DeleteActionButton; import de.danoeh.antennapod.event.DownloadLogEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.storage.database.DBReader; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.dialog.ItemSortDialog; import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; @@ -179,7 +179,7 @@ public class CompletedDownloadsFragment extends Fragment @Override public boolean onMenuItemClick(MenuItem item) { if (item.getItemId() == R.id.refresh_item) { - FeedUpdateManager.runOnceOrAsk(requireContext()); + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); return true; } else if (item.getItemId() == R.id.action_download_logs) { new DownloadLogFragment().show(getChildFragmentManager(), null); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java index c963c8298..54c6d1a9b 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/DownloadLogFragment.java @@ -15,7 +15,7 @@ import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DownloadLogAdapter; import de.danoeh.antennapod.event.DownloadLogEvent; 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.databinding.DownloadLogFragmentBinding; import de.danoeh.antennapod.dialog.DownloadLogDetailsDialog; import de.danoeh.antennapod.model.download.DownloadResult; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java index 08714e971..0f9e21b6e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java @@ -24,6 +24,7 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.snackbar.Snackbar; import com.leinardi.android.speeddial.SpeedDialView; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -38,7 +39,6 @@ import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; import de.danoeh.antennapod.event.FeedListUpdateEvent; @@ -122,7 +122,7 @@ public abstract class EpisodesListFragment extends Fragment public boolean onMenuItemClick(MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.refresh_item) { - FeedUpdateManager.runOnceOrAsk(requireContext()); + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); return true; } else if (itemId == R.id.action_search) { ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); @@ -182,7 +182,7 @@ public abstract class EpisodesListFragment extends Fragment swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.runOnceOrAsk(requireContext())); + swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); listAdapter = new EpisodeItemListAdapter((MainActivity) getActivity()) { @Override diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java index 49ea28765..c93837851 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedInfoFragment.java @@ -35,7 +35,7 @@ import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.FeedDatabaseWriter; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; import de.danoeh.antennapod.core.util.syndication.HtmlToPlainText; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 680036aea..1884012ea 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -26,6 +26,7 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.snackbar.Snackbar; import com.leinardi.android.speeddial.SpeedDialView; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.greenrobot.eventbus.EventBus; @@ -42,12 +43,11 @@ import de.danoeh.antennapod.adapter.EpisodeItemListAdapter; import de.danoeh.antennapod.event.FeedEvent; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; 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.storage.database.FeedItemPermutors; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.ShareUtils; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil; import de.danoeh.antennapod.databinding.FeedItemListFragmentBinding; import de.danoeh.antennapod.databinding.MultiSelectSpeedDialBinding; @@ -171,7 +171,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem nextPageLoader = new MoreContentListFooterUtil(viewBinding.moreContent.moreContentListFooter); nextPageLoader.setClickListener(() -> { if (feed != null) { - FeedUpdateManager.runOnce(getContext(), feed, true); + FeedUpdateManager.getInstance().runOnce(getContext(), feed, true); } }); viewBinding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @@ -190,7 +190,8 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem EventBus.getDefault().register(this); viewBinding.swipeRefresh.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - viewBinding.swipeRefresh.setOnRefreshListener(() -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed)); + viewBinding.swipeRefresh.setOnRefreshListener(() -> + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext(), feed)); loadItems(); @@ -273,14 +274,14 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem } else if (item.getItemId() == R.id.share_item) { ShareUtils.shareFeedLink(getContext(), feed); } else if (item.getItemId() == R.id.refresh_item) { - FeedUpdateManager.runOnceOrAsk(getContext(), feed); + FeedUpdateManager.getInstance().runOnceOrAsk(getContext(), feed); } else if (item.getItemId() == R.id.refresh_complete_item) { new Thread(() -> { feed.setNextPageLink(feed.getDownloadUrl()); feed.setPageNr(0); try { DBWriter.resetPagedFeedPage(feed).get(); - FeedUpdateManager.runOnce(getContext(), feed); + FeedUpdateManager.getInstance().runOnce(getContext(), feed); } catch (ExecutionException | InterruptedException e) { throw new RuntimeException(e); } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java index 23a3489e0..69cfb0087 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedSettingsFragment.java @@ -26,7 +26,6 @@ import androidx.preference.PreferenceFragmentCompat; import androidx.preference.SwitchPreferenceCompat; import androidx.recyclerview.widget.RecyclerView; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent; import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent; import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent; @@ -35,9 +34,10 @@ import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedFilter; import de.danoeh.antennapod.model.feed.FeedPreferences; import de.danoeh.antennapod.model.feed.VolumeAdaptionSetting; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.storage.preferences.UserPreferences; 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.dialog.EpisodeFilterDialog; import de.danoeh.antennapod.dialog.FeedPreferenceSkipDialog; import de.danoeh.antennapod.dialog.TagSettingsDialog; @@ -313,7 +313,7 @@ public class FeedSettingsFragment extends Fragment { } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } - FeedUpdateManager.runOnce(getContext(), feed); + FeedUpdateManager.getInstance().runOnce(getContext(), feed); }, "RefreshAfterCredentialChange").start(); } }.show(); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java index 259d92a99..40ce3f823 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/InboxFragment.java @@ -15,7 +15,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; 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.dialog.ItemSortDialog; import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.model.feed.FeedItem; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java index bc741f492..49ef099f9 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/NavDrawerFragment.java @@ -52,7 +52,7 @@ import de.danoeh.antennapod.adapter.NavListAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; 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.storage.database.NavDrawerData; import de.danoeh.antennapod.dialog.DrawerPreferencesDialog; import de.danoeh.antennapod.dialog.RemoveFeedDialog; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index 2faa5ad62..18263b90f 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -10,7 +10,7 @@ import androidx.annotation.NonNull; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; 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.PlaybackHistoryEvent; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index 65e16dc5b..126d0d748 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -28,6 +28,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.leinardi.android.speeddial.SpeedDialView; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.ui.episodes.PlaybackSpeedUtils; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -42,10 +43,9 @@ import de.danoeh.antennapod.adapter.QueueRecyclerAdapter; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; 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.ui.common.Converter; import de.danoeh.antennapod.core.util.FeedItemUtil; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.dialog.ItemSortDialog; import de.danoeh.antennapod.event.EpisodeDownloadEvent; import de.danoeh.antennapod.event.FeedItemEvent; @@ -279,7 +279,7 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte new QueueSortDialog().show(getChildFragmentManager().beginTransaction(), "SortDialog"); return true; } else if (itemId == R.id.refresh_item) { - FeedUpdateManager.runOnceOrAsk(requireContext()); + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); return true; } else if (itemId == R.id.clear_queue) { // make sure the user really wants to clear the queue @@ -428,7 +428,7 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.runOnceOrAsk(requireContext())); + swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); emptyView = new EmptyViewHandler(getContext()); emptyView.attachToRecyclerView(recyclerView); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java index 299117aec..dcc78f152 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/SubscriptionFragment.java @@ -22,6 +22,7 @@ import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.leinardi.android.speeddial.SpeedDialView; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; @@ -36,7 +37,6 @@ import de.danoeh.antennapod.adapter.SubscriptionsRecyclerAdapter; import de.danoeh.antennapod.core.menuhandler.MenuItemUtils; import de.danoeh.antennapod.storage.database.DBReader; import de.danoeh.antennapod.storage.database.NavDrawerData; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.dialog.FeedSortDialog; import de.danoeh.antennapod.dialog.RenameItemDialog; import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog; @@ -170,7 +170,7 @@ public class SubscriptionFragment extends Fragment swipeRefreshLayout = root.findViewById(R.id.swipeRefresh); swipeRefreshLayout.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.runOnceOrAsk(requireContext())); + swipeRefreshLayout.setOnRefreshListener(() -> FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); speedDialView = root.findViewById(R.id.fabSD); speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay)); @@ -204,7 +204,7 @@ public class SubscriptionFragment extends Fragment public boolean onMenuItemClick(MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.refresh_item) { - FeedUpdateManager.runOnceOrAsk(requireContext()); + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); return true; } else if (itemId == R.id.subscriptions_filter) { new SubscriptionsFilterDialog().show(getChildFragmentManager(), "filter"); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java index ad23b8188..9325037ad 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java @@ -11,7 +11,7 @@ import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.storage.database.LongList; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.view.LocalDeleteModal; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java index b5a9215e6..5a6b4ffa9 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/actions/FeedMultiSelectActionHandler.java @@ -14,7 +14,7 @@ import java.util.Locale; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.databinding.PlaybackSpeedFeedSettingDialogBinding; import de.danoeh.antennapod.dialog.RemoveFeedDialog; import de.danoeh.antennapod.dialog.TagSettingsDialog; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java index 27028eaa2..58e968155 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/DownloadsPreferencesFragment.java @@ -10,7 +10,7 @@ import androidx.preference.TwoStatePreference; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.PreferenceActivity; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.ui.preferences.screen.downloads.ChooseDataFolderDialog; import de.danoeh.antennapod.dialog.ProxyDialog; import de.danoeh.antennapod.storage.preferences.UserPreferences; @@ -90,7 +90,7 @@ public class DownloadsPreferencesFragment extends PreferenceFragmentCompat @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (UserPreferences.PREF_UPDATE_INTERVAL.equals(key)) { - FeedUpdateManager.restartUpdateAlarm(getContext(), true); + FeedUpdateManager.getInstance().restartUpdateAlarm(getContext(), true); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java index 729f35291..06efda3ee 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java @@ -5,7 +5,7 @@ import android.content.Context; import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; -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.FeedItemFilter; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/DeleteSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/DeleteSwipeAction.java index 462fcf968..52f214eed 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/DeleteSwipeAction.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/DeleteSwipeAction.java @@ -6,7 +6,7 @@ import androidx.fragment.app.Fragment; import java.util.Collections; import de.danoeh.antennapod.R; -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.FeedItemFilter; import de.danoeh.antennapod.view.LocalDeleteModal; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java index 162003fab..dcea8c031 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java @@ -5,7 +5,7 @@ import android.content.Context; import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; -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.FeedItemFilter; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromHistorySwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromHistorySwipeAction.java index 385beb2dd..46285734e 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromHistorySwipeAction.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromHistorySwipeAction.java @@ -10,7 +10,7 @@ import java.util.Date; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; -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.FeedItemFilter; diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java index f4d2bb6c8..f5cbf66c6 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java @@ -9,7 +9,7 @@ import com.google.android.material.snackbar.Snackbar; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; 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.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedItemFilter; diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java index b4b6112ff..2465b162b 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -16,12 +16,12 @@ import java.util.Arrays; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.util.FeedUtil; import de.danoeh.antennapod.core.service.playback.PlaybackServiceInterface; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.core.util.FeedItemUtil; import de.danoeh.antennapod.core.util.IntentUtils; import de.danoeh.antennapod.core.util.PlaybackStatus; @@ -30,6 +30,7 @@ import de.danoeh.antennapod.dialog.ShareDialog; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.net.sync.model.EpisodeAction; +import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; import de.danoeh.antennapod.view.LocalDeleteModal; @@ -230,8 +231,14 @@ public class FeedItemMenuHandler { final Handler h = new Handler(fragment.requireContext().getMainLooper()); final Runnable r = () -> { FeedMedia media = item.getMedia(); + if (media == null) { + return; + } boolean shouldAutoDelete = FeedUtil.shouldAutoDeleteItemsOnThatFeed(item.getFeed()); - if (media != null && FeedItemUtil.hasAlmostEnded(media) && shouldAutoDelete) { + int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); + boolean almostEnded = media.getDuration() > 0 + && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000; + if (almostEnded && shouldAutoDelete) { DBWriter.deleteFeedMediaOfItem(fragment.requireContext(), media); } }; diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java index eac1e9304..c0448884d 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedMenuHandler.java @@ -8,7 +8,7 @@ import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.dialog.RemoveFeedDialog; import de.danoeh.antennapod.dialog.RenameItemDialog; import de.danoeh.antennapod.dialog.TagSettingsDialog; diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java index 9b3b40b45..641355d5c 100644 --- a/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java +++ b/app/src/main/java/de/danoeh/antennapod/receiver/PowerConnectionReceiver.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.util.Log; import de.danoeh.antennapod.core.ClientConfigurator; -import de.danoeh.antennapod.core.storage.AutoDownloadManager; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; @@ -32,7 +32,7 @@ public class PowerConnectionReceiver extends BroadcastReceiver { // downloading now. They shouldn't mind. // autodownloadUndownloadedItems will make sure we're on the right wifi networks, // etc... so we don't have to worry about it. - AutoDownloadManager.autodownloadUndownloadedItems(context); + AutoDownloadManager.getInstance().autodownloadUndownloadedItems(context); } else { // if we're not supposed to be auto-downloading when we're not charging, stop it if (!UserPreferences.isEnableAutodownloadOnBattery()) { diff --git a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java index d6641a1e1..a48ee117b 100644 --- a/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java +++ b/app/src/main/java/de/danoeh/antennapod/receiver/SPAReceiver.java @@ -12,8 +12,8 @@ import java.util.Collections; import de.danoeh.antennapod.R; import de.danoeh.antennapod.core.ClientConfigurator; -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.model.feed.Feed; /** @@ -49,6 +49,6 @@ public class SPAReceiver extends BroadcastReceiver{ FeedDatabaseWriter.updateFeed(context, feed, false); } Toast.makeText(context, R.string.sp_apps_importing_feeds_msg, Toast.LENGTH_LONG).show(); - FeedUpdateManager.runOnce(context); + FeedUpdateManager.getInstance().runOnce(context); } } diff --git a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java index 0837b5e8d..90d4817da 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/home/HomeFragment.java @@ -19,6 +19,7 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentContainerView; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; import de.danoeh.antennapod.ui.echo.EchoConfig; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -32,7 +33,6 @@ import java.util.List; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.util.download.FeedUpdateManager; import de.danoeh.antennapod.databinding.HomeFragmentBinding; import de.danoeh.antennapod.event.FeedListUpdateEvent; import de.danoeh.antennapod.event.FeedUpdateRunningEvent; @@ -83,7 +83,8 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis updateWelcomeScreenVisibility(); viewBinding.swipeRefresh.setDistanceToTriggerSync(getResources().getInteger(R.integer.swipe_refresh_distance)); - viewBinding.swipeRefresh.setOnRefreshListener(() -> FeedUpdateManager.runOnceOrAsk(requireContext())); + viewBinding.swipeRefresh.setOnRefreshListener(() -> + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext())); return viewBinding.getRoot(); } @@ -156,7 +157,7 @@ public class HomeFragment extends Fragment implements Toolbar.OnMenuItemClickLis HomeSectionsSettingsDialog.open(getContext(), (dialogInterface, i) -> populateSectionList()); return true; } else if (item.getItemId() == R.id.refresh_item) { - FeedUpdateManager.runOnceOrAsk(requireContext()); + FeedUpdateManager.getInstance().runOnceOrAsk(requireContext()); return true; } else if (item.getItemId() == R.id.action_search) { ((MainActivity) getActivity()).loadChildFragment(SearchFragment.newInstance()); diff --git a/core/build.gradle b/core/build.gradle index fbf6654e5..b2fffa68b 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -30,6 +30,8 @@ dependencies { implementation project(':net:ssl') implementation project(':net:sync:gpoddernet') implementation project(':net:sync:model') + implementation project(':net:sync:service') + implementation project(':net:sync:service-interface') implementation project(':parser:feed') implementation project(':parser:media') implementation project(':playback:base') 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 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/AutoDownloadManager.java deleted file mode 100644 index bc5244381..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManager.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import android.content.Context; -import android.util.Log; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -public abstract class AutoDownloadManager { - private static final String TAG = "AutoDownloadManager"; - - /** - * Executor service used by the autodownloadUndownloadedEpisodes method. - */ - private static final ExecutorService autodownloadExec; - - private static AutomaticDownloadAlgorithm downloadAlgorithm = new AutomaticDownloadAlgorithm(); - - static { - autodownloadExec = Executors.newSingleThreadExecutor(r -> { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - }); - } - - /** - * Looks for non-downloaded episodes in the queue or list of unread items and request a download if - * 1. Network is available - * 2. The device is charging or the user allows auto download on battery - * 3. There is free space in the episode cache - * This method is executed on an internal single thread executor. - * - * @param context Used for accessing the DB. - * @return A Future that can be used for waiting for the methods completion. - */ - public static Future autodownloadUndownloadedItems(final Context context) { - Log.d(TAG, "autodownloadUndownloadedItems"); - return autodownloadExec.submit(downloadAlgorithm.autoDownloadUndownloadedItems(context)); - } - - /** - * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller - * 'playbackCompletionDate'-value will be deleted first. - *

- * This method should NOT be executed on the GUI thread. - * - * @param context Used for accessing the DB. - */ - public static void performAutoCleanup(final Context context) { - EpisodeCleanupAlgorithmFactory.build().performCleanup(context); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManagerImpl.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManagerImpl.java new file mode 100644 index 000000000..b00375ffe --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutoDownloadManagerImpl.java @@ -0,0 +1,55 @@ +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 class AutoDownloadManagerImpl extends AutoDownloadManager { + private static final String TAG = "AutoDownloadManager"; + + /** + * Executor service used by the autodownloadUndownloadedEpisodes method. + */ + private static final ExecutorService autodownloadExec; + + private static AutomaticDownloadAlgorithm downloadAlgorithm = new AutomaticDownloadAlgorithm(); + + static { + autodownloadExec = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + }); + } + + /** + * Looks for non-downloaded episodes in the queue or list of unread items and request a download if + * 1. Network is available + * 2. The device is charging or the user allows auto download on battery + * 3. There is free space in the episode cache + * This method is executed on an internal single thread executor. + * + * @param context Used for accessing the DB. + * @return A Future that can be used for waiting for the methods completion. + */ + public Future autodownloadUndownloadedItems(final Context context) { + Log.d(TAG, "autodownloadUndownloadedItems"); + return autodownloadExec.submit(downloadAlgorithm.autoDownloadUndownloadedItems(context)); + } + + /** + * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller + * 'playbackCompletionDate'-value will be deleted first. + *

+ * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + */ + public void performAutoCleanup(final Context context) { + EpisodeCleanupAlgorithmFactory.build().performCleanup(context); + } +} diff --git a/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 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 items) { - List queue = DBReader.getQueue(); - List 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 media - */ - 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 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 queue = DBReader.getQueue(adapter); - - boolean queueModified = false; - LongList markAsUnplayedIds = new LongList(); - List events = new ArrayList<>(); - List 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 queue, List 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 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 queue = DBReader.getQueue(adapter); - - if (queue != null) { - boolean queueModified = false; - List events = new ArrayList<>(); - List 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. - *

- * 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 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 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 items, long itemId) { - return indexInItemList(items, itemId) >= 0; - } - - private static int indexInItemList(List 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 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. - */ - 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 permutor = FeedItemPermutors.getPermutor(sortOrder); - return runOnDbThread(() -> { - final PodDBAdapter adapter = PodDBAdapter.getInstance(); - adapter.open(); - final List 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 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 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 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 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 unlistedItems = new ArrayList<>(); - List 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 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 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 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 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 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, EpisodeAction> getRemoteActionsOverridingLocalActions( - List remoteActions, - List queuedEpisodeActions) { - // make sure more recent local actions are not overwritten by older remote actions - Map, EpisodeAction> remoteActionsThatOverrideLocalActions = new ArrayMap<>(); - Map, EpisodeAction> localMostRecentPlayActions = - createUniqueLocalMostRecentPlayActions(queuedEpisodeActions); - for (EpisodeAction remoteAction : remoteActions) { - Pair 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, EpisodeAction> createUniqueLocalMostRecentPlayActions( - List queuedEpisodeActions) { - Map, EpisodeAction> localMostRecentPlayAction; - localMostRecentPlayAction = new ArrayMap<>(); - for (EpisodeAction action : queuedEpisodeActions) { - Pair 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 localSubscriptions = DBReader.getFeedListDownloadUrls(); - SubscriptionChanges subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync); - long newTimeStamp = subscriptionChanges.getTimestamp(); - - List queuedRemovedFeeds = synchronizationQueueStorage.getQueuedRemovedFeeds(); - List 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 remoteActions = getResponse.getEpisodeActions(); - processEpisodeActions(remoteActions); - - // upload local actions - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_upload)); - List queuedEpisodeActions = synchronizationQueueStorage.getQueuedEpisodeActions(); - if (lastSync == 0) { - EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_upload_played)); - List 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 remoteActions) { - Log.d(TAG, "Processing " + remoteActions.size() + " actions"); - if (remoteActions.size() == 0) { - return; - } - - Map, EpisodeAction> playActionsToUpdate = EpisodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, - synchronizationQueueStorage.getQueuedEpisodeActions()); - LongList queueToBeRemoved = new LongList(); - List 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 getQueuedEpisodeActions() { - ArrayList 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 getQueuedRemovedFeeds() { - ArrayList 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 getQueuedAddedFeeds() { - ArrayList 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 { - - /** - * 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/FeedUpdateManager.java deleted file mode 100644 index 89097d9ee..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManager.java +++ /dev/null @@ -1,121 +0,0 @@ -package de.danoeh.antennapod.core.util.download; - -import android.content.Context; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.OutOfQuotaPolicy; -import androidx.work.PeriodicWorkRequest; -import androidx.work.WorkManager; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import de.danoeh.antennapod.core.R; -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.storage.preferences.UserPreferences; -import org.greenrobot.eventbus.EventBus; - -import java.util.concurrent.TimeUnit; - -public class 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"; - public static final String EXTRA_FEED_ID = "feed_id"; - public static final String EXTRA_NEXT_PAGE = "next_page"; - 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) { - if (UserPreferences.isAutoUpdateDisabled()) { - WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE); - } else { - PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder( - FeedUpdateWorker.class, UserPreferences.getUpdateInterval(), TimeUnit.HOURS) - .setConstraints(new Constraints.Builder() - .setRequiredNetworkType(UserPreferences.isAllowMobileFeedRefresh() - ? NetworkType.CONNECTED : NetworkType.UNMETERED).build()) - .build(); - WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE, - replace ? ExistingPeriodicWorkPolicy.REPLACE : ExistingPeriodicWorkPolicy.KEEP, workRequest); - } - } - - public static void runOnce(Context context) { - runOnce(context, null, false); - } - - public static void runOnce(Context context, Feed feed) { - runOnce(context, feed, false); - } - - public static 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) - .addTag(WORK_TAG_FEED_UPDATE); - if (feed == null || !feed.isLocalFeed()) { - workRequest.setConstraints(new Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED).build()); - } - Data.Builder builder = new Data.Builder(); - builder.putBoolean(EXTRA_EVEN_ON_MOBILE, true); - if (feed != null) { - builder.putLong(EXTRA_FEED_ID, feed.getId()); - builder.putBoolean(EXTRA_NEXT_PAGE, nextPage); - } - workRequest.setInputData(builder.build()); - WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_MANUAL, - ExistingWorkPolicy.REPLACE, workRequest.build()); - } - - public static void runOnceOrAsk(@NonNull Context context) { - runOnceOrAsk(context, null); - } - - public static 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); - } else if (!NetworkUtils.networkAvailable()) { - EventBus.getDefault().post(new MessageEvent(context.getString(R.string.download_error_no_connection))); - } else if (NetworkUtils.isFeedRefreshAllowed()) { - runOnce(context, feed); - } else { - confirmMobileRefresh(context, feed); - } - } - - private static 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, - (dialog, which) -> runOnce(context, feed)) - .setNeutralButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { - UserPreferences.setAllowMobileFeedRefresh(true); - runOnce(context, feed); - }) - .setNegativeButton(R.string.no, null); - if (NetworkUtils.isNetworkRestricted() && NetworkUtils.isVpnOverWifi()) { - builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message_vpn); - } else { - builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message); - } - builder.show(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java new file mode 100644 index 000000000..17077c237 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/download/FeedUpdateManagerImpl.java @@ -0,0 +1,118 @@ +package de.danoeh.antennapod.core.util.download; + +import android.content.Context; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.work.Constraints; +import androidx.work.Data; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.ExistingWorkPolicy; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.OutOfQuotaPolicy; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import de.danoeh.antennapod.core.R; +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 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"; + public static final String EXTRA_FEED_ID = "feed_id"; + public static final String EXTRA_NEXT_PAGE = "next_page"; + public static final String EXTRA_EVEN_ON_MOBILE = "even_on_mobile"; + private static final String TAG = "AutoUpdateManager"; + + /** + * Start / restart periodic auto feed refresh + * @param context Context + */ + public void restartUpdateAlarm(Context context, boolean replace) { + if (UserPreferences.isAutoUpdateDisabled()) { + WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE); + } else { + PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder( + FeedUpdateWorker.class, UserPreferences.getUpdateInterval(), TimeUnit.HOURS) + .setConstraints(new Constraints.Builder() + .setRequiredNetworkType(UserPreferences.isAllowMobileFeedRefresh() + ? NetworkType.CONNECTED : NetworkType.UNMETERED).build()) + .build(); + WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE, + replace ? ExistingPeriodicWorkPolicy.REPLACE : ExistingPeriodicWorkPolicy.KEEP, workRequest); + } + } + + public void runOnce(Context context) { + runOnce(context, null, false); + } + + public void runOnce(Context context, Feed feed) { + runOnce(context, feed, false); + } + + 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) + .addTag(WORK_TAG_FEED_UPDATE); + if (feed == null || !feed.isLocalFeed()) { + workRequest.setConstraints(new Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED).build()); + } + Data.Builder builder = new Data.Builder(); + builder.putBoolean(EXTRA_EVEN_ON_MOBILE, true); + if (feed != null) { + builder.putLong(EXTRA_FEED_ID, feed.getId()); + builder.putBoolean(EXTRA_NEXT_PAGE, nextPage); + } + workRequest.setInputData(builder.build()); + WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_MANUAL, + ExistingWorkPolicy.REPLACE, workRequest.build()); + } + + public void runOnceOrAsk(@NonNull Context context) { + runOnceOrAsk(context, null); + } + + 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); + } else if (!NetworkUtils.networkAvailable()) { + EventBus.getDefault().post(new MessageEvent(context.getString(R.string.download_error_no_connection))); + } else if (NetworkUtils.isFeedRefreshAllowed()) { + runOnce(context, feed); + } else { + confirmMobileRefresh(context, 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, + (dialog, which) -> runOnce(context, feed)) + .setNeutralButton(R.string.confirm_mobile_streaming_button_always, (dialog, which) -> { + UserPreferences.setAllowMobileFeedRefresh(true); + runOnce(context, feed); + }) + .setNegativeButton(R.string.no, null); + if (NetworkUtils.isNetworkRestricted() && NetworkUtils.isVpnOverWifi()) { + builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message_vpn); + } else { + builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message); + } + builder.show(); + } +} 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 deleted file mode 100644 index cd133aa98..000000000 Binary files a/core/src/main/res/drawable-nodpi/gpodder_icon.png and /dev/null differ diff --git a/core/src/main/res/drawable-nodpi/nextcloud_logo.png b/core/src/main/res/drawable-nodpi/nextcloud_logo.png deleted file mode 100644 index 2164e37fb..000000000 Binary files a/core/src/main/res/drawable-nodpi/nextcloud_logo.png and /dev/null differ 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 @@ - - diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java index 12256264a..9703894f5 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java @@ -35,7 +35,7 @@ import java.util.Objects; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.storage.database.DBReader; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import static org.hamcrest.CoreMatchers.endsWith; import static org.hamcrest.CoreMatchers.is; diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java index a7d435aed..a29a87b05 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbCleanupTests.java @@ -15,6 +15,8 @@ import androidx.test.platform.app.InstrumentationRegistry; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.storage.preferences.PlaybackPreferences; import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; import de.danoeh.antennapod.storage.preferences.UserPreferences; @@ -80,6 +82,7 @@ public class DbCleanupTests { UserPreferences.init(context); PlaybackPreferences.init(context); SynchronizationSettings.init(context); + AutoDownloadManager.setInstance(new AutoDownloadManagerImpl()); } @After @@ -108,7 +111,7 @@ public class DbCleanupTests { List files = new ArrayList<>(); populateItems(numItems, feed, items, files, FeedItem.PLAYED, false, false); - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { if (i < EPISODE_CACHE_SIZE) { assertTrue(files.get(i).exists()); @@ -167,7 +170,7 @@ public class DbCleanupTests { List files = new ArrayList<>(); populateItems(numItems, feed, items, files, FeedItem.UNPLAYED, false, false); - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (File file : files) { assertTrue(file.exists()); } @@ -183,7 +186,7 @@ public class DbCleanupTests { List files = new ArrayList<>(); populateItems(numItems, feed, items, files, FeedItem.PLAYED, true, false); - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (File file : files) { assertTrue(file.exists()); } @@ -223,7 +226,7 @@ public class DbCleanupTests { List files = new ArrayList<>(); populateItems(numItems, feed, items, files, FeedItem.PLAYED, false, true); - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (File file : files) { assertTrue(file.exists()); } diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java index 243bef941..bbcf362b9 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbNullCleanupAlgorithmTest.java @@ -14,6 +14,8 @@ import androidx.test.platform.app.InstrumentationRegistry; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.storage.database.PodDBAdapter; import org.junit.After; @@ -61,6 +63,7 @@ public class DbNullCleanupAlgorithmTest { prefEdit.commit(); UserPreferences.init(context); + AutoDownloadManager.setInstance(new AutoDownloadManagerImpl()); } @After @@ -114,7 +117,7 @@ public class DbNullCleanupAlgorithmTest { //noinspection ConstantConditions assertTrue(item.getMedia().getId() != 0); } - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { assertTrue(files.get(i).exists()); } diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbQueueCleanupAlgorithmTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbQueueCleanupAlgorithmTest.java index c14c3c2aa..04762b123 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbQueueCleanupAlgorithmTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbQueueCleanupAlgorithmTest.java @@ -7,6 +7,7 @@ import java.util.List; import de.danoeh.antennapod.model.feed.Feed; import de.danoeh.antennapod.model.feed.FeedItem; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; import de.danoeh.antennapod.storage.preferences.UserPreferences; import org.junit.Test; @@ -24,6 +25,7 @@ public class DbQueueCleanupAlgorithmTest extends DbCleanupTests { public DbQueueCleanupAlgorithmTest() { setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_QUEUE); + AutoDownloadManager.setInstance(new AutoDownloadManagerImpl()); } /** @@ -40,7 +42,7 @@ public class DbQueueCleanupAlgorithmTest extends DbCleanupTests { List files = new ArrayList<>(); populateItems(numItems, feed, items, files, FeedItem.UNPLAYED, false, false); - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { if (i < EPISODE_CACHE_SIZE) { assertTrue(files.get(i).exists()); diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java index 99db467bf..d37e0d7e1 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbReaderTest.java @@ -19,6 +19,7 @@ import de.danoeh.antennapod.model.feed.FeedMedia; import de.danoeh.antennapod.model.feed.FeedOrder; import de.danoeh.antennapod.model.feed.SortOrder; import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.storage.database.NavDrawerData; import de.danoeh.antennapod.storage.preferences.UserPreferences; import de.danoeh.antennapod.storage.database.LongList; diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java index 075e7a559..1f90dd7a9 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbTasksTest.java @@ -5,6 +5,8 @@ import android.content.Context; import androidx.test.platform.app.InstrumentationRegistry; import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; import de.danoeh.antennapod.storage.database.PodDBAdapter; import org.junit.After; import org.junit.Before; diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java index 4fea67f79..90876cc11 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/DbWriterTest.java @@ -12,6 +12,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfaceStub; import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.storage.database.PodDBAdapter; import org.awaitility.Awaitility; import org.junit.After; diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java index b464a2508..df26b9e4f 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ExceptFavoriteCleanupAlgorithmTest.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.storage; +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.RobolectricTestRunner; @@ -25,6 +26,7 @@ public class ExceptFavoriteCleanupAlgorithmTest extends DbCleanupTests { public ExceptFavoriteCleanupAlgorithmTest() { setCleanupAlgorithm(UserPreferences.EPISODE_CLEANUP_EXCEPT_FAVORITE); + AutoDownloadManager.setInstance(new AutoDownloadManagerImpl()); } @Test @@ -35,7 +37,7 @@ public class ExceptFavoriteCleanupAlgorithmTest extends DbCleanupTests { List files = new ArrayList<>(); populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, false, false); - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { if (i < EPISODE_CACHE_SIZE) { assertTrue("Only enough items should be deleted", files.get(i).exists()); @@ -53,7 +55,7 @@ public class ExceptFavoriteCleanupAlgorithmTest extends DbCleanupTests { List files = new ArrayList<>(); populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, true, false); - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { if (i < EPISODE_CACHE_SIZE) { assertTrue("Only enough items should be deleted", files.get(i).exists()); @@ -71,7 +73,7 @@ public class ExceptFavoriteCleanupAlgorithmTest extends DbCleanupTests { List files = new ArrayList<>(); populateItems(numberOfItems, feed, items, files, FeedItem.UNPLAYED, false, true); - AutoDownloadManager.performAutoCleanup(context); + AutoDownloadManager.getInstance().performAutoCleanup(context); for (int i = 0; i < files.size(); i++) { assertTrue("Favorite episodes should should not be deleted", files.get(i).exists()); } diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/FeedItemDuplicateGuesserTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/FeedItemDuplicateGuesserTest.java index f3c993066..38dbe8caa 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/FeedItemDuplicateGuesserTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/FeedItemDuplicateGuesserTest.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.storage; import de.danoeh.antennapod.model.feed.FeedItem; import de.danoeh.antennapod.model.feed.FeedMedia; +import de.danoeh.antennapod.storage.database.FeedItemDuplicateGuesser; import org.junit.Test; import java.util.Date; diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java index f0f81eee1..dfad714a8 100644 --- a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.storage; import de.danoeh.antennapod.model.playback.RemoteMedia; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterfaceStub; +import de.danoeh.antennapod.storage.database.ItemEnqueuePositionCalculator; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; diff --git a/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java b/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java deleted file mode 100644 index 1f638bf32..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/sync/EpisodeActionFilterTest.java +++ /dev/null @@ -1,212 +0,0 @@ -package de.danoeh.antennapod.core.sync; - - -import androidx.core.util.Pair; - -import junit.framework.TestCase; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Map; - -import de.danoeh.antennapod.net.sync.model.EpisodeAction; - - -public class EpisodeActionFilterTest extends TestCase { - - EpisodeActionFilter episodeActionFilter = new EpisodeActionFilter(); - - public void testGetRemoteActionsHappeningAfterLocalActions() throws ParseException { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date morning = format.parse("2021-01-01 08:00:00"); - Date lateMorning = format.parse("2021-01-01 09:00:00"); - - List episodeActions = new ArrayList<>(); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(morning) - .position(10) - .build() - ); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(lateMorning) - .position(20) - .build() - ); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) - .timestamp(morning) - .position(5) - .build() - ); - - Date morningFiveMinutesLater = format.parse("2021-01-01 08:05:00"); - List remoteActions = new ArrayList<>(); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(morningFiveMinutesLater) - .position(10) - .build() - ); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) - .timestamp(morningFiveMinutesLater) - .position(5) - .build() - ); - - Map, EpisodeAction> uniqueList = episodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); - assertSame(1, uniqueList.size()); - } - - public void testGetRemoteActionsHappeningBeforeLocalActions() throws ParseException { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date morning = format.parse("2021-01-01 08:00:00"); - Date lateMorning = format.parse("2021-01-01 09:00:00"); - - List episodeActions = new ArrayList<>(); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(morning) - .position(10) - .build() - ); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(lateMorning) - .position(20) - .build() - ); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) - .timestamp(morning) - .position(5) - .build() - ); - - Date morningFiveMinutesEarlier = format.parse("2021-01-01 07:55:00"); - List remoteActions = new ArrayList<>(); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(morningFiveMinutesEarlier) - .position(10) - .build() - ); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) - .timestamp(morningFiveMinutesEarlier) - .position(5) - .build() - ); - - Map, EpisodeAction> uniqueList = episodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); - assertSame(0, uniqueList.size()); - } - - public void testGetMultipleRemoteActionsHappeningAfterLocalActions() throws ParseException { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date morning = format.parse("2021-01-01 08:00:00"); - - List episodeActions = new ArrayList<>(); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(morning) - .position(10) - .build() - ); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) - .timestamp(morning) - .position(5) - .build() - ); - - Date morningFiveMinutesLater = format.parse("2021-01-01 08:05:00"); - List remoteActions = new ArrayList<>(); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(morningFiveMinutesLater) - .position(10) - .build() - ); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) - .timestamp(morningFiveMinutesLater) - .position(5) - .build() - ); - - Map, EpisodeAction> uniqueList = episodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); - assertEquals(2, uniqueList.size()); - } - - public void testGetMultipleRemoteActionsHappeningBeforeLocalActions() throws ParseException { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date morning = format.parse("2021-01-01 08:00:00"); - - List episodeActions = new ArrayList<>(); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(morning) - .position(10) - .build() - ); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) - .timestamp(morning) - .position(5) - .build() - ); - - Date morningFiveMinutesEarlier = format.parse("2021-01-01 07:55:00"); - List remoteActions = new ArrayList<>(); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(morningFiveMinutesEarlier) - .position(10) - .build() - ); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) - .timestamp(morningFiveMinutesEarlier) - .position(5) - .build() - ); - - Map, EpisodeAction> uniqueList = episodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); - assertEquals(0, uniqueList.size()); - } - - public void testPresentRemoteTimestampOverridesMissingLocalTimestamp() throws ParseException { - SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - Date arbitraryTime = format.parse("2021-01-01 08:00:00"); - - List episodeActions = new ArrayList<>(); - episodeActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - // no timestamp - .position(10) - .build() - ); - - List remoteActions = new ArrayList<>(); - remoteActions.add(new EpisodeAction - .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) - .timestamp(arbitraryTime) - .position(10) - .build() - ); - - Map, EpisodeAction> uniqueList = episodeActionFilter - .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); - assertSame(1, uniqueList.size()); - } -} diff --git a/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java deleted file mode 100644 index 552f7d70a..000000000 --- a/core/src/test/java/de/danoeh/antennapod/core/sync/GuidValidatorTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.danoeh.antennapod.core.sync; - -import junit.framework.TestCase; - -public class GuidValidatorTest extends TestCase { - - public void testIsValidGuid() { - assertTrue(GuidValidator.isValidGuid("skfjsdvgsd")); - } - - public void testIsInvalidGuid() { - assertFalse(GuidValidator.isValidGuid("")); - assertFalse(GuidValidator.isValidGuid(" ")); - assertFalse(GuidValidator.isValidGuid("\n")); - assertFalse(GuidValidator.isValidGuid(" \n")); - assertFalse(GuidValidator.isValidGuid(null)); - assertFalse(GuidValidator.isValidGuid("null")); - } -} \ No newline at end of file diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/AutoDownloadManager.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/AutoDownloadManager.java new file mode 100644 index 000000000..2eb1d1b56 --- /dev/null +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/AutoDownloadManager.java @@ -0,0 +1,39 @@ +package de.danoeh.antennapod.net.download.serviceinterface; + +import android.content.Context; + +import java.util.concurrent.Future; + +public abstract class AutoDownloadManager { + private static AutoDownloadManager instance; + + public static AutoDownloadManager getInstance() { + return instance; + } + + public static void setInstance(AutoDownloadManager instance) { + AutoDownloadManager.instance = instance; + } + + /** + * Looks for non-downloaded episodes in the queue or list of unread items and request a download if + * 1. Network is available + * 2. The device is charging or the user allows auto download on battery + * 3. There is free space in the episode cache + * This method is executed on an internal single thread executor. + * + * @param context Used for accessing the DB. + * @return A Future that can be used for waiting for the methods completion. + */ + public abstract Future autodownloadUndownloadedItems(final Context context); + + /** + * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller + * 'playbackCompletionDate'-value will be deleted first. + *

+ * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + */ + public abstract void performAutoCleanup(final Context context); +} diff --git a/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/FeedUpdateManager.java b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/FeedUpdateManager.java new file mode 100644 index 000000000..25a8f42d3 --- /dev/null +++ b/net/download/service-interface/src/main/java/de/danoeh/antennapod/net/download/serviceinterface/FeedUpdateManager.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.net.download.serviceinterface; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import de.danoeh.antennapod.model.feed.Feed; + +public abstract class FeedUpdateManager { + private static FeedUpdateManager instance; + + public static FeedUpdateManager getInstance() { + return instance; + } + + public static void setInstance(FeedUpdateManager instance) { + FeedUpdateManager.instance = instance; + } + + public abstract void restartUpdateAlarm(Context context, boolean replace); + + public abstract void runOnce(Context context); + + public abstract void runOnce(Context context, Feed feed); + + public abstract void runOnce(Context context, Feed feed, boolean nextPage); + + public abstract void runOnceOrAsk(@NonNull Context context); + + public abstract void runOnceOrAsk(@NonNull Context context, @Nullable Feed feed); +} diff --git a/net/sync/service-interface/README.md b/net/sync/service-interface/README.md new file mode 100644 index 000000000..2b6a3c412 --- /dev/null +++ b/net/sync/service-interface/README.md @@ -0,0 +1,3 @@ +# :net:sync:service-interface + +This module contains the interface for starting the sync service. diff --git a/net/sync/service-interface/build.gradle b/net/sync/service-interface/build.gradle new file mode 100644 index 000000000..c1a559da3 --- /dev/null +++ b/net/sync/service-interface/build.gradle @@ -0,0 +1,19 @@ +plugins { + id("com.android.library") +} +apply from: "../../../common.gradle" + +android { + namespace "de.danoeh.antennapod.net.sync.serviceinterface" +} + +dependencies { + implementation project(':model') + implementation project(':net:sync:model') + implementation project(':storage:preferences') + implementation project(':ui:i18n') + + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" + implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" +} diff --git a/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/LockingAsyncExecutor.java b/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/LockingAsyncExecutor.java new file mode 100644 index 000000000..2703cf018 --- /dev/null +++ b/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/LockingAsyncExecutor.java @@ -0,0 +1,43 @@ +package de.danoeh.antennapod.net.sync.serviceinterface; + +import java.util.concurrent.locks.ReentrantLock; + +import io.reactivex.Completable; +import io.reactivex.schedulers.Schedulers; + +public class LockingAsyncExecutor { + + private 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(); + } + } + + public static void unlock() { + lock.unlock(); + } + + public static void lock() { + lock.lock(); + } +} diff --git a/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationProviderViewData.java b/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationProviderViewData.java new file mode 100644 index 000000000..19624a95a --- /dev/null +++ b/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationProviderViewData.java @@ -0,0 +1,45 @@ +package de.danoeh.antennapod.net.sync.serviceinterface; + +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/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationQueueSink.java b/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationQueueSink.java new file mode 100644 index 000000000..8c94c44e7 --- /dev/null +++ b/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationQueueSink.java @@ -0,0 +1,81 @@ +package de.danoeh.antennapod.net.sync.serviceinterface; + +import android.content.Context; + +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/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationQueueStorage.java b/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationQueueStorage.java new file mode 100644 index 000000000..0ae794ac8 --- /dev/null +++ b/net/sync/service-interface/src/main/java/de/danoeh/antennapod/net/sync/serviceinterface/SynchronizationQueueStorage.java @@ -0,0 +1,158 @@ +package de.danoeh.antennapod.net.sync.serviceinterface; + +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 getQueuedEpisodeActions() { + ArrayList 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 getQueuedRemovedFeeds() { + ArrayList 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 getQueuedAddedFeeds() { + ArrayList 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/net/sync/service-interface/src/main/res/drawable-nodpi/gpodder_icon.png b/net/sync/service-interface/src/main/res/drawable-nodpi/gpodder_icon.png new file mode 100644 index 000000000..cd133aa98 Binary files /dev/null and b/net/sync/service-interface/src/main/res/drawable-nodpi/gpodder_icon.png differ diff --git a/net/sync/service-interface/src/main/res/drawable-nodpi/nextcloud_logo.png b/net/sync/service-interface/src/main/res/drawable-nodpi/nextcloud_logo.png new file mode 100644 index 000000000..2164e37fb Binary files /dev/null and b/net/sync/service-interface/src/main/res/drawable-nodpi/nextcloud_logo.png differ diff --git a/net/sync/service-interface/src/main/res/values/ids.xml b/net/sync/service-interface/src/main/res/values/ids.xml new file mode 100644 index 000000000..842e421ea --- /dev/null +++ b/net/sync/service-interface/src/main/res/values/ids.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/net/sync/service/README.md b/net/sync/service/README.md new file mode 100644 index 000000000..e4ce70c58 --- /dev/null +++ b/net/sync/service/README.md @@ -0,0 +1,3 @@ +# :net:sync:service + +This module contains the sync service. diff --git a/net/sync/service/build.gradle b/net/sync/service/build.gradle new file mode 100644 index 000000000..7fa19d320 --- /dev/null +++ b/net/sync/service/build.gradle @@ -0,0 +1,36 @@ +plugins { + id("com.android.library") +} +apply from: "../../../common.gradle" +apply from: "../../../playFlavor.gradle" + +android { + namespace "de.danoeh.antennapod.net.sync.service" +} + +dependencies { + implementation project(':event') + implementation project(':model') + implementation project(':net:common') + implementation project(':net:sync:gpoddernet') + implementation project(':net:sync:model') + implementation project(':net:sync:service-interface') + implementation project(':storage:database') + implementation project(':storage:preferences') + implementation project(':ui:notifications') + implementation project(':ui:i18n') + implementation project(':net:download:service-interface') + + annotationProcessor "androidx.annotation:annotation:$annotationVersion" + implementation "androidx.core:core:$coreVersion" + implementation "androidx.work:work-runtime:$workManagerVersion" + + implementation "org.greenrobot:eventbus:$eventbusVersion" + implementation "org.apache.commons:commons-lang3:$commonslangVersion" + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" + implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" + implementation "com.google.guava:guava:31.0.1-android" + + testImplementation "junit:junit:$junitVersion" +} diff --git a/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/EpisodeActionFilter.java b/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/EpisodeActionFilter.java new file mode 100644 index 000000000..42fc1b7b9 --- /dev/null +++ b/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/EpisodeActionFilter.java @@ -0,0 +1,77 @@ +package de.danoeh.antennapod.net.sync.service; + +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, EpisodeAction> getRemoteActionsOverridingLocalActions( + List remoteActions, + List queuedEpisodeActions) { + // make sure more recent local actions are not overwritten by older remote actions + Map, EpisodeAction> remoteActionsThatOverrideLocalActions = new ArrayMap<>(); + Map, EpisodeAction> localMostRecentPlayActions = + createUniqueLocalMostRecentPlayActions(queuedEpisodeActions); + for (EpisodeAction remoteAction : remoteActions) { + Pair 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, EpisodeAction> createUniqueLocalMostRecentPlayActions( + List queuedEpisodeActions) { + Map, EpisodeAction> localMostRecentPlayAction; + localMostRecentPlayAction = new ArrayMap<>(); + for (EpisodeAction action : queuedEpisodeActions) { + Pair 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/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/GuidValidator.java b/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/GuidValidator.java new file mode 100644 index 000000000..98ae8037e --- /dev/null +++ b/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/GuidValidator.java @@ -0,0 +1,11 @@ +package de.danoeh.antennapod.net.sync.service; + +public class GuidValidator { + + public static boolean isValidGuid(String guid) { + return guid != null + && !guid.trim().isEmpty() + && !guid.equals("null"); + } +} + diff --git a/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/SyncService.java b/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/SyncService.java new file mode 100644 index 000000000..f52c2b81d --- /dev/null +++ b/net/sync/service/src/main/java/de/danoeh/antennapod/net/sync/service/SyncService.java @@ -0,0 +1,390 @@ +package de.danoeh.antennapod.net.sync.service; + +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.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.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.net.sync.serviceinterface.LockingAsyncExecutor; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationProviderViewData; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueStorage; +import de.danoeh.antennapod.storage.database.DBWriter; +import de.danoeh.antennapod.storage.database.FeedDatabaseWriter; +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.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.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 localSubscriptions = DBReader.getFeedListDownloadUrls(); + SubscriptionChanges subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync); + long newTimeStamp = subscriptionChanges.getTimestamp(); + + List queuedRemovedFeeds = synchronizationQueueStorage.getQueuedRemovedFeeds(); + List 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.getInstance().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(); + try { + UploadChangesResponse uploadResponse = syncServiceImpl + .uploadSubscriptionChanges(queuedAddedFeeds, queuedRemovedFeeds); + synchronizationQueueStorage.clearFeedQueues(); + newTimeStamp = uploadResponse.timestamp; + } finally { + LockingAsyncExecutor.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 remoteActions = getResponse.getEpisodeActions(); + processEpisodeActions(remoteActions); + + // upload local actions + EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_episodes_upload)); + List queuedEpisodeActions = synchronizationQueueStorage.getQueuedEpisodeActions(); + if (lastSync == 0) { + EventBus.getDefault().postSticky(new SyncServiceEvent(R.string.sync_status_upload_played)); + List 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.isEmpty()) { + LockingAsyncExecutor.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.unlock(); + } + } + SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp); + } + + private synchronized void processEpisodeActions(List remoteActions) { + Log.d(TAG, "Processing " + remoteActions.size() + " actions"); + if (remoteActions.size() == 0) { + return; + } + + Map, EpisodeAction> playActionsToUpdate = EpisodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, + synchronizationQueueStorage.getQueuedEpisodeActions()); + LongList queueToBeRemoved = new LongList(); + List 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; + } + FeedMedia media = feedItem.getMedia(); + media.setPosition(action.getPosition() * 1000); + int smartMarkAsPlayedSecs = UserPreferences.getSmartMarkAsPlayedSecs(); + boolean almostEnded = media.getDuration() > 0 + && media.getPosition() >= media.getDuration() - smartMarkAsPlayedSecs * 1000; + if (almostEnded) { + Log.d(TAG, "Marking as played: " + action); + feedItem.setPlayed(true); + media.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/net/sync/service/src/test/java/de/danoeh/antennapod/net/sync/service/EpisodeActionFilterTest.java b/net/sync/service/src/test/java/de/danoeh/antennapod/net/sync/service/EpisodeActionFilterTest.java new file mode 100644 index 000000000..22ea316d4 --- /dev/null +++ b/net/sync/service/src/test/java/de/danoeh/antennapod/net/sync/service/EpisodeActionFilterTest.java @@ -0,0 +1,212 @@ +package de.danoeh.antennapod.net.sync.service; + + +import androidx.core.util.Pair; + +import junit.framework.TestCase; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import de.danoeh.antennapod.net.sync.model.EpisodeAction; + + +public class EpisodeActionFilterTest extends TestCase { + + EpisodeActionFilter episodeActionFilter = new EpisodeActionFilter(); + + public void testGetRemoteActionsHappeningAfterLocalActions() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date morning = format.parse("2021-01-01 08:00:00"); + Date lateMorning = format.parse("2021-01-01 09:00:00"); + + List episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(10) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(lateMorning) + .position(20) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(5) + .build() + ); + + Date morningFiveMinutesLater = format.parse("2021-01-01 08:05:00"); + List remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesLater) + .position(10) + .build() + ); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesLater) + .position(5) + .build() + ); + + Map, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertSame(1, uniqueList.size()); + } + + public void testGetRemoteActionsHappeningBeforeLocalActions() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date morning = format.parse("2021-01-01 08:00:00"); + Date lateMorning = format.parse("2021-01-01 09:00:00"); + + List episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(10) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(lateMorning) + .position(20) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(5) + .build() + ); + + Date morningFiveMinutesEarlier = format.parse("2021-01-01 07:55:00"); + List remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesEarlier) + .position(10) + .build() + ); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesEarlier) + .position(5) + .build() + ); + + Map, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertSame(0, uniqueList.size()); + } + + public void testGetMultipleRemoteActionsHappeningAfterLocalActions() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date morning = format.parse("2021-01-01 08:00:00"); + + List episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(10) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(5) + .build() + ); + + Date morningFiveMinutesLater = format.parse("2021-01-01 08:05:00"); + List remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesLater) + .position(10) + .build() + ); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesLater) + .position(5) + .build() + ); + + Map, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertEquals(2, uniqueList.size()); + } + + public void testGetMultipleRemoteActionsHappeningBeforeLocalActions() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date morning = format.parse("2021-01-01 08:00:00"); + + List episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(10) + .build() + ); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morning) + .position(5) + .build() + ); + + Date morningFiveMinutesEarlier = format.parse("2021-01-01 07:55:00"); + List remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesEarlier) + .position(10) + .build() + ); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.2", EpisodeAction.Action.PLAY) + .timestamp(morningFiveMinutesEarlier) + .position(5) + .build() + ); + + Map, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertEquals(0, uniqueList.size()); + } + + public void testPresentRemoteTimestampOverridesMissingLocalTimestamp() throws ParseException { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + Date arbitraryTime = format.parse("2021-01-01 08:00:00"); + + List episodeActions = new ArrayList<>(); + episodeActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + // no timestamp + .position(10) + .build() + ); + + List remoteActions = new ArrayList<>(); + remoteActions.add(new EpisodeAction + .Builder("podcast.a", "episode.1", EpisodeAction.Action.PLAY) + .timestamp(arbitraryTime) + .position(10) + .build() + ); + + Map, EpisodeAction> uniqueList = episodeActionFilter + .getRemoteActionsOverridingLocalActions(remoteActions, episodeActions); + assertSame(1, uniqueList.size()); + } +} diff --git a/net/sync/service/src/test/java/de/danoeh/antennapod/net/sync/service/GuidValidatorTest.java b/net/sync/service/src/test/java/de/danoeh/antennapod/net/sync/service/GuidValidatorTest.java new file mode 100644 index 000000000..3e3d77a1f --- /dev/null +++ b/net/sync/service/src/test/java/de/danoeh/antennapod/net/sync/service/GuidValidatorTest.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.net.sync.service; + +import junit.framework.TestCase; + +public class GuidValidatorTest extends TestCase { + + public void testIsValidGuid() { + assertTrue(GuidValidator.isValidGuid("skfjsdvgsd")); + } + + public void testIsInvalidGuid() { + assertFalse(GuidValidator.isValidGuid("")); + assertFalse(GuidValidator.isValidGuid(" ")); + assertFalse(GuidValidator.isValidGuid("\n")); + assertFalse(GuidValidator.isValidGuid(" \n")); + assertFalse(GuidValidator.isValidGuid(null)); + assertFalse(GuidValidator.isValidGuid("null")); + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index b23f2dc20..e81206376 100644 --- a/settings.gradle +++ b/settings.gradle @@ -26,6 +26,8 @@ include ':net:download:service-interface' include ':net:ssl' include ':net:sync:gpoddernet' include ':net:sync:model' +include ':net:sync:service-interface' +include ':net:sync:service' include ':parser:feed' include ':parser:media' diff --git a/storage/database/build.gradle b/storage/database/build.gradle index 0f3aed252..63f9eeaec 100644 --- a/storage/database/build.gradle +++ b/storage/database/build.gradle @@ -2,6 +2,7 @@ plugins { id("com.android.library") } apply from: "../../common.gradle" +apply from: "../../playFlavor.gradle" android { namespace "de.danoeh.antennapod.storage.database" @@ -12,11 +13,21 @@ android { } dependencies { + implementation project(':event') implementation project(':model') + implementation project(':net:download:service-interface') + implementation project(':net:sync:model') + implementation project(':net:sync:service-interface') + implementation project(':storage:preferences') + implementation project(':ui:app-start-intent') annotationProcessor "androidx.annotation:annotation:$annotationVersion" implementation "androidx.core:core:$coreVersion" + implementation 'androidx.documentfile:documentfile:1.0.1' + implementation "commons-io:commons-io:$commonsioVersion" + implementation "org.greenrobot:eventbus:$eventbusVersion" + implementation "com.google.guava:guava:31.0.1-android" testImplementation "junit:junit:$junitVersion" } diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBWriter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBWriter.java new file mode 100644 index 000000000..0d7cf5cb1 --- /dev/null +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBWriter.java @@ -0,0 +1,1055 @@ +package de.danoeh.antennapod.storage.database; + +import android.app.backup.BackupManager; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import android.view.KeyEvent; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; + +import com.google.common.util.concurrent.Futures; +import de.danoeh.antennapod.event.DownloadLogEvent; + +import de.danoeh.antennapod.net.download.serviceinterface.AutoDownloadManager; +import de.danoeh.antennapod.net.download.serviceinterface.DownloadServiceInterface; +import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; +import de.danoeh.antennapod.ui.appstartintent.MediaButtonStarter; +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.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.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(); + context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_STOP)); + } + + if (localDelete) { + // Do full update of this feed to get rid of the item + FeedUpdateManager.getInstance().runOnce(context, media.getItem().getFeed()); + } 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 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 items) { + List queue = DBReader.getQueue(); + List 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(); + context.sendBroadcast(MediaButtonStarter.createIntent(context, KeyEvent.KEYCODE_MEDIA_STOP)); + } + 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 media + */ + 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 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.getInstance().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 queue = DBReader.getQueue(adapter); + + boolean queueModified = false; + LongList markAsUnplayedIds = new LongList(); + List events = new ArrayList<>(); + List 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.getInstance().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 queue, List 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 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 queue = DBReader.getQueue(adapter); + + if (queue != null) { + boolean queueModified = false; + List events = new ArrayList<>(); + List 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.getInstance().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. + *

+ * 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 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 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 items, long itemId) { + return indexInItemList(items, itemId) >= 0; + } + + private static int indexInItemList(List 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 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. + */ + 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 permutor = FeedItemPermutors.getPermutor(sortOrder); + return runOnDbThread(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + final List 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 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/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedDatabaseWriter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedDatabaseWriter.java new file mode 100644 index 000000000..eb33f5705 --- /dev/null +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedDatabaseWriter.java @@ -0,0 +1,259 @@ +package de.danoeh.antennapod.storage.database; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; +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.net.sync.serviceinterface.SynchronizationQueueSink; +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 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 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 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 unlistedItems = new ArrayList<>(); + List 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 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/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemDuplicateGuesser.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemDuplicateGuesser.java new file mode 100644 index 000000000..bbaedb519 --- /dev/null +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemDuplicateGuesser.java @@ -0,0 +1,83 @@ +package de.danoeh.antennapod.storage.database; + +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/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemPubdateComparator.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemPubdateComparator.java new file mode 100644 index 000000000..3838a47ac --- /dev/null +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/FeedItemPubdateComparator.java @@ -0,0 +1,27 @@ +package de.danoeh.antennapod.storage.database; + +import java.util.Comparator; + +import de.danoeh.antennapod.model.feed.FeedItem; + +/** + * Compares the pubDate of two FeedItems for sorting. + */ +public class FeedItemPubdateComparator implements Comparator { + + /** + * 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/storage/database/src/main/java/de/danoeh/antennapod/storage/database/ItemEnqueuePositionCalculator.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/ItemEnqueuePositionCalculator.java new file mode 100644 index 000000000..55c2b079c --- /dev/null +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/ItemEnqueuePositionCalculator.java @@ -0,0 +1,94 @@ +package de.danoeh.antennapod.storage.database; + +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.database.DBWriter; +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. + */ +public 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 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 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 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 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/storage/importexport/build.gradle b/storage/importexport/build.gradle index ae1faa284..dfe5fa535 100644 --- a/storage/importexport/build.gradle +++ b/storage/importexport/build.gradle @@ -2,6 +2,7 @@ plugins { id("com.android.library") } apply from: "../../common.gradle" +apply from: "../../playFlavor.gradle" android { namespace "de.danoeh.antennapod.storage.importexport" @@ -24,4 +25,5 @@ dependencies { implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion" implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" implementation "org.greenrobot:eventbus:$eventbusVersion" + implementation "com.google.guava:guava:31.0.1-android" } diff --git a/ui/app-start-intent/src/main/res/values/pending_intent.xml b/ui/app-start-intent/src/main/res/values/pending_intent.xml index 30b35d926..b9a6ac65f 100644 --- a/ui/app-start-intent/src/main/res/values/pending_intent.xml +++ b/ui/app-start-intent/src/main/res/values/pending_intent.xml @@ -11,5 +11,4 @@ - diff --git a/ui/preferences/build.gradle b/ui/preferences/build.gradle index c1f56854f..e462c55e6 100644 --- a/ui/preferences/build.gradle +++ b/ui/preferences/build.gradle @@ -34,6 +34,8 @@ dependencies { implementation project(":ui:common") implementation project(":ui:glide") implementation project(":ui:i18n") + implementation project(':net:sync:service-interface') + implementation project(':net:sync:service') annotationProcessor "androidx.annotation:annotation:$annotationVersion" implementation "androidx.appcompat:appcompat:$appcompatVersion" diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java index 6328bc61e..53f620fc3 100644 --- a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/GpodderAuthenticationFragment.java @@ -18,11 +18,11 @@ import androidx.annotation.Nullable; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.fragment.app.DialogFragment; import com.google.android.material.button.MaterialButton; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.net.sync.service.SyncService; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationProviderViewData; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import de.danoeh.antennapod.storage.preferences.SynchronizationCredentials; -import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData; import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; import de.danoeh.antennapod.core.util.FileNameGenerator; import de.danoeh.antennapod.net.sync.gpoddernet.GpodnetService; diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java index d0ecb8a24..c503ccef1 100644 --- a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/NextcloudAuthenticationFragment.java @@ -8,11 +8,11 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.fragment.app.DialogFragment; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; import de.danoeh.antennapod.net.common.AntennapodHttpClient; -import de.danoeh.antennapod.core.sync.SyncService; +import de.danoeh.antennapod.net.sync.service.SyncService; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationProviderViewData; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import de.danoeh.antennapod.storage.preferences.SynchronizationCredentials; -import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData; import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; import de.danoeh.antennapod.net.sync.nextcloud.NextcloudLoginFlow; import de.danoeh.antennapod.ui.preferences.R; diff --git a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java index d4c8bee72..c8c993cbc 100644 --- a/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java +++ b/ui/preferences/src/main/java/de/danoeh/antennapod/ui/preferences/screen/synchronization/SynchronizationPreferencesFragment.java @@ -21,7 +21,9 @@ import androidx.preference.PreferenceFragmentCompat; import com.google.android.material.snackbar.Snackbar; -import de.danoeh.antennapod.core.sync.queue.SynchronizationQueueSink; +import de.danoeh.antennapod.net.sync.service.SyncService; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationProviderViewData; +import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink; import de.danoeh.antennapod.ui.preferences.R; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; @@ -29,8 +31,6 @@ import org.greenrobot.eventbus.ThreadMode; import de.danoeh.antennapod.event.SyncServiceEvent; import de.danoeh.antennapod.storage.preferences.SynchronizationCredentials; -import de.danoeh.antennapod.core.sync.SyncService; -import de.danoeh.antennapod.core.sync.SynchronizationProviderViewData; import de.danoeh.antennapod.storage.preferences.SynchronizationSettings; public class SynchronizationPreferencesFragment extends PreferenceFragmentCompat { diff --git a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java index 1c35b159b..c5ec4cb59 100644 --- a/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java +++ b/ui/statistics/src/main/java/de/danoeh/antennapod/ui/statistics/StatisticsFragment.java @@ -19,7 +19,7 @@ import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import de.danoeh.antennapod.core.dialog.ConfirmationDialog; -import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.storage.database.DBWriter; import de.danoeh.antennapod.event.StatisticsEvent; import de.danoeh.antennapod.ui.common.PagedToolbarFragment; import de.danoeh.antennapod.ui.echo.EchoActivity; diff --git a/ui/widget/build.gradle b/ui/widget/build.gradle index 03183a323..3cf054c7e 100644 --- a/ui/widget/build.gradle +++ b/ui/widget/build.gradle @@ -32,4 +32,5 @@ dependencies { implementation "androidx.appcompat:appcompat:$appcompatVersion" implementation "androidx.work:work-runtime:$workManagerVersion" implementation "com.github.bumptech.glide:glide:$glideVersion" + implementation "com.google.guava:guava:31.0.1-android" } -- cgit v1.2.3