From c49af77af879c522ad65a4f52b0dc714a7c6519e Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Fri, 17 May 2024 23:05:16 +0100 Subject: [PATCH] 5.3.0 commit --- README.md | 5 +- app/build.gradle | 24 +- .../PlaybackServiceTaskManagerTest.kt | 67 +++-- .../java/ac/test/podcini/ui/UITestUtils.kt | 19 +- .../util/event/FeedItemEventListener.kt | 41 --- .../main/java/ac/mdiq/podcini/PodciniApp.kt | 29 +-- .../java/ac/mdiq/podcini/feed/FeedEvent.kt | 18 -- .../podcini/net/discovery/CombinedSearcher.kt | 5 +- .../podcini/net/download/FeedUpdateManager.kt | 23 +- .../service/DownloadServiceInterfaceImpl.kt | 58 +++-- .../download/service/EpisodeDownloadWorker.kt | 15 +- .../service/handler/MediaDownloadedHandler.kt | 6 +- .../podcini/net/sync/LockingAsyncExecutor.kt | 30 ++- .../ac/mdiq/podcini/net/sync/SyncService.kt | 60 +++-- .../net/sync/nextcloud/NextcloudLoginFlow.kt | 66 +++-- .../podcini/net/sync/wifi/WifiSyncService.kt | 34 +-- .../podcini/playback/PlaybackController.kt | 32 ++- .../playback/service/LocalMediaPlayer.kt | 27 +- .../playback/service/PlaybackService.kt | 88 +++---- .../service/PlaybackServiceTaskManager.kt | 14 +- .../preferences/PlaybackPreferences.kt | 6 +- .../podcini/preferences/UserPreferences.kt | 16 +- .../ImportExportPreferencesFragment.kt | 92 +++++-- .../fragments/PlaybackPreferencesFragment.kt | 6 +- .../UserInterfacePreferencesFragment.kt | 9 +- .../fragments/about/DevelopersFragment.kt | 34 +-- .../fragments/about/LicensesFragment.kt | 43 ++-- .../fragments/about/SpecialThanksFragment.kt | 37 ++- .../fragments/about/TranslatorsFragment.kt | 28 +- .../GpodderAuthenticationFragment.kt | 111 +++++--- .../SynchronizationPreferencesFragment.kt | 47 ++-- .../WifiAuthenticationFragment.kt | 26 +- .../java/ac/mdiq/podcini/storage/DBReader.kt | 8 +- .../java/ac/mdiq/podcini/storage/DBTasks.kt | 18 +- .../java/ac/mdiq/podcini/storage/DBWriter.kt | 168 ++++++------ .../podcini/storage/asynctask/ExportWorker.kt | 1 + .../podcini/storage/database/PodDBAdapter.kt | 9 + .../database/mapper/FeedItemSortQuery.kt | 6 + .../podcini/storage/model/feed/SortOrder.kt | 6 +- .../actions/actionbutton/PlayActionButton.kt | 6 +- .../actionbutton/StreamActionButton.kt | 19 +- .../actions/actionbutton/TTSActionButton.kt | 18 +- .../ui/actions/swipeactions/SwipeActions.kt | 18 +- .../mdiq/podcini/ui/activity/MainActivity.kt | 36 ++- .../podcini/ui/activity/OpmlImportActivity.kt | 200 ++++++++++----- .../podcini/ui/activity/PreferenceActivity.kt | 27 +- .../ui/activity/SelectSubscriptionActivity.kt | 5 +- .../podcini/ui/activity/SplashActivity.kt | 68 +++-- .../ui/activity/VideoplayerActivity.kt | 45 ++-- .../ui/adapter/EpisodeItemListAdapter.kt | 3 +- .../ui/dialog/AllEpisodesFilterDialog.kt | 8 +- .../ui/dialog/DownloadLogDetailsDialog.kt | 18 +- .../mdiq/podcini/ui/dialog/FeedSortDialog.kt | 12 +- .../mdiq/podcini/ui/dialog/ItemSortDialog.kt | 20 +- .../ui/dialog/MediaPlayerErrorDialog.kt | 4 +- .../ac/mdiq/podcini/ui/dialog/ProxyDialog.kt | 170 +++++++----- .../podcini/ui/dialog/SleepTimerDialog.kt | 38 ++- .../ui/dialog/SubscriptionsFilterDialog.kt | 20 +- .../podcini/ui/dialog/TagSettingsDialog.kt | 6 +- .../podcini/ui/dialog/VariableSpeedDialog.kt | 31 ++- .../mdiq/podcini/ui/dialog/VideoModeDialog.kt | 10 +- .../ui/fragment/AllEpisodesFragment.kt | 37 ++- .../ui/fragment/AudioPlayerFragment.kt | 106 ++++---- .../ui/fragment/BaseEpisodesListFragment.kt | 136 +++++----- .../podcini/ui/fragment/ChaptersFragment.kt | 82 +++--- .../podcini/ui/fragment/DiscoveryFragment.kt | 15 +- .../ui/fragment/DownloadLogFragment.kt | 36 ++- .../podcini/ui/fragment/DownloadsFragment.kt | 84 +++--- .../ui/fragment/EpisodeInfoFragment.kt | 65 +++-- ...ent.kt => ExternalEpisodesListFragment.kt} | 33 ++- .../podcini/ui/fragment/FeedInfoFragment.kt | 5 +- .../ui/fragment/FeedItemlistFragment.kt | 111 ++++---- .../ui/fragment/FeedSettingsFragment.kt | 25 +- .../podcini/ui/fragment/NavDrawerFragment.kt | 50 ++-- .../ui/fragment/OnlineFeedViewFragment.kt | 53 ++-- .../ui/fragment/PlaybackHistoryFragment.kt | 149 +++++++++-- .../ui/fragment/PlayerDetailsFragment.kt | 35 ++- .../mdiq/podcini/ui/fragment/QueueFragment.kt | 100 ++++---- .../ui/fragment/QuickFeedDiscoveryFragment.kt | 39 ++- .../podcini/ui/fragment/SearchFragment.kt | 70 ++--- .../ui/fragment/SubscriptionFragment.kt | 64 +++-- .../ui/fragment/VideoEpisodeFragment.kt | 50 ++-- .../ui/statistics/StatisticsFragment.kt | 36 ++- .../downloads/DownloadStatisticsFragment.kt | 57 +++-- .../statistics/feed/FeedStatisticsFragment.kt | 75 +++--- .../subscriptions/DatesFilterDialog.kt | 134 ++++++++++ .../subscriptions/StatisticsFilterDialog.kt | 141 ---------- .../SubscriptionStatisticsFragment.kt | 122 ++++++--- .../years/YearsStatisticsFragment.kt | 65 +++-- .../view/viewholder/EpisodeItemViewHolder.kt | 20 +- .../mdiq/podcini/ui/widget/WidgetUpdater.kt | 20 +- .../ac/mdiq/podcini/util/FeedItemPermutors.kt | 34 ++- .../java/ac/mdiq/podcini/util/NetworkUtils.kt | 4 +- .../PlaybackLastPlayedDateComparator.kt | 12 + .../util/event/DiscoveryDefaultUpdateEvent.kt | 3 - .../podcini/util/event/DownloadLogEvent.kt | 14 - .../util/event/EpisodeDownloadEvent.kt | 8 - .../mdiq/podcini/util/event/FavoritesEvent.kt | 4 - .../mdiq/podcini/util/event/FeedItemEvent.kt | 18 -- .../podcini/util/event/FeedListUpdateEvent.kt | 25 -- .../util/event/FeedTagsChangedEvent.kt | 3 - .../util/event/FeedUpdateRunningEvent.kt | 3 - .../ac/mdiq/podcini/util/event/FlowEvent.kt | 241 ++++++++++++++++++ .../mdiq/podcini/util/event/MessageEvent.kt | 9 - .../podcini/util/event/PlayerErrorEvent.kt | 3 - .../podcini/util/event/PlayerStatusEvent.kt | 4 - .../ac/mdiq/podcini/util/event/QueueEvent.kt | 50 ---- .../podcini/util/event/StatisticsEvent.kt | 3 - .../util/event/SwipeActionsChangedEvent.kt | 3 - .../podcini/util/event/SyncServiceEvent.kt | 3 - .../util/event/UnreadItemsUpdateEvent.kt | 4 - .../util/event/playback/BufferUpdateEvent.kt | 27 -- .../event/playback/PlaybackHistoryEvent.kt | 14 - .../event/playback/PlaybackPositionEvent.kt | 3 - .../event/playback/PlaybackServiceEvent.kt | 9 - .../event/playback/SleepTimerUpdatedEvent.kt | 35 --- .../util/event/playback/SpeedChangedEvent.kt | 3 - .../util/event/playback/StartPlayEvent.kt | 5 - .../settings/SkipIntroEndingChangedEvent.kt | 3 - .../event/settings/SpeedPresetChangedEvent.kt | 3 - .../settings/VolumeAdaptionChangedEvent.kt | 5 - .../res/layout/statistics_filter_dialog.xml | 1 + app/src/main/res/menu/playback_history.xml | 17 +- app/src/main/res/values/strings.xml | 4 +- .../ac/mdiq/podcini/playback/cast/CastPsmp.kt | 15 +- changelog.md | 11 + .../android/en-US/changelogs/3020145.txt | 11 + 127 files changed, 2761 insertions(+), 1982 deletions(-) delete mode 100644 app/src/androidTest/java/ac/test/podcini/util/event/FeedItemEventListener.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/feed/FeedEvent.kt rename app/src/main/java/ac/mdiq/podcini/ui/fragment/{EpisodesListFragment.kt => ExternalEpisodesListFragment.kt} (85%) create mode 100644 app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/DatesFilterDialog.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/StatisticsFilterDialog.kt create mode 100644 app/src/main/java/ac/mdiq/podcini/util/comparator/PlaybackLastPlayedDateComparator.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/DiscoveryDefaultUpdateEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/DownloadLogEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/EpisodeDownloadEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/FavoritesEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/FeedItemEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/FeedListUpdateEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/FeedTagsChangedEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/FeedUpdateRunningEvent.kt create mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/FlowEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/MessageEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/PlayerErrorEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/PlayerStatusEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/QueueEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/StatisticsEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/SwipeActionsChangedEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/SyncServiceEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/UnreadItemsUpdateEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/playback/BufferUpdateEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackHistoryEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackPositionEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackServiceEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/playback/SleepTimerUpdatedEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/playback/SpeedChangedEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/playback/StartPlayEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/settings/SkipIntroEndingChangedEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/settings/SpeedPresetChangedEvent.kt delete mode 100644 app/src/main/java/ac/mdiq/podcini/util/event/settings/VolumeAdaptionChangedEvent.kt create mode 100644 fastlane/metadata/android/en-US/changelogs/3020145.txt diff --git a/README.md b/README.md index 63320cc0..3ad531df 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Compared to AntennaPod this project: 2. Plays in `AudioOffloadMode`, kind to device battery, 3. Is purely `Kotlin` based and mono-modular, 4. Targets Android 14 with updated dependencies, -5. Outfits with Viewbinding and modern image library Coil, +5. Outfits with Viewbinding, Coil replacing Glide, coroutines replacing RxJava, and SharedFlow replacing EventBus, 6. Boasts new UI's including streamlined drawer, subscriptions view and player controller, 7. Accepts podcast as well as plain RSS and YouTube feeds, 8. Offers Readability and Text-to-Speech for RSS contents, @@ -72,6 +72,7 @@ The project aims to improve efficiency and provide more useful and user-friendly * Sort dialog no longer dims the main view * in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward) * Subscriptions view has sorting by "Unread publication date" +* History view shows time of last play, and allows filters and sorts ### Podcast/Episode @@ -100,7 +101,7 @@ The project aims to improve efficiency and provide more useful and user-friendly ### Security and reliability * Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure -* Settings/Preferences can now to exported and imported +* Settings/Preferences can now be exported and imported For more details of the changes, see the [Changelog](changelog.md) diff --git a/app/build.gradle b/app/build.gradle index 5adecd0b..86be3516 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ plugins { id('com.android.application') id 'kotlin-android' - id 'kotlin-kapt' +// id 'kotlin-kapt' id 'com.google.devtools.ksp' id('com.github.triplet.play') version '3.8.3' apply false } @@ -159,8 +159,8 @@ android { // Version code schema (not used): // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3020144 - versionName "5.2.1" + versionCode 3020145 + versionName "5.3.0" def commit = "" try { @@ -232,9 +232,7 @@ dependencies { } } -// doesn't work with ksp?? - kapt "androidx.annotation:annotation:1.8.0" - + implementation "androidx.annotation:annotation:1.8.0" implementation "androidx.appcompat:appcompat:1.6.1" implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation "androidx.fragment:fragment-ktx:1.6.2" @@ -266,8 +264,8 @@ dependencies { implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0" implementation 'com.squareup.okio:okio:3.9.0' - implementation "org.greenrobot:eventbus:3.3.1" - kapt "org.greenrobot:eventbus-annotation-processor:3.3.1" +// implementation "org.greenrobot:eventbus:3.3.1" +// kapt "org.greenrobot:eventbus-annotation-processor:3.3.1" implementation "io.reactivex.rxjava2:rxandroid:2.1.1" implementation "io.reactivex.rxjava2:rxjava:2.2.21" @@ -316,11 +314,11 @@ dependencies { playApi 'com.google.android.gms:play-services-cast-framework:21.4.0' } -kapt { - arguments { - arg('eventBusIndex', 'ac.mdiq.podcini.ApEventBusIndex') - } -} +//kapt { +// arguments { +// arg('eventBusIndex', 'ac.mdiq.podcini.ApEventBusIndex') +// } +//} if (project.hasProperty("podciniPlayPublisherCredentials")) { apply plugin: 'com.github.triplet.play' diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceTaskManagerTest.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceTaskManagerTest.kt index 4fcd9eac..ae6554bf 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceTaskManagerTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/PlaybackServiceTaskManagerTest.kt @@ -1,23 +1,26 @@ package de.test.podcini.service.playback -import androidx.test.annotation.UiThreadTest -import androidx.test.filters.LargeTest -import androidx.test.platform.app.InstrumentationRegistry -import ac.mdiq.podcini.preferences.SleepTimerPreferences.setShakeToReset -import ac.mdiq.podcini.preferences.SleepTimerPreferences.setVibrate +import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.service.PlaybackServiceTaskManager import ac.mdiq.podcini.playback.service.PlaybackServiceTaskManager.PSTMCallback -import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState -import ac.mdiq.podcini.util.event.playback.SleepTimerUpdatedEvent -import ac.mdiq.podcini.storage.model.feed.Feed -import ac.mdiq.podcini.storage.model.feed.FeedItem -import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.playback.base.PlayerStatus +import ac.mdiq.podcini.preferences.SleepTimerPreferences.setShakeToReset +import ac.mdiq.podcini.preferences.SleepTimerPreferences.setVibrate import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.init -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe +import ac.mdiq.podcini.storage.model.feed.Feed +import ac.mdiq.podcini.storage.model.feed.FeedItem +import ac.mdiq.podcini.storage.model.playback.Playable +import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent +import androidx.test.annotation.UiThreadTest +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.junit.After import org.junit.Assert import org.junit.Before @@ -31,6 +34,9 @@ import java.util.concurrent.TimeUnit */ @LargeTest class PlaybackServiceTaskManagerTest { + + val scope = CoroutineScope(Dispatchers.Main) + @After fun tearDown() { deleteDatabase() @@ -205,19 +211,29 @@ class PlaybackServiceTaskManagerTest { val TIMEOUT = 2 * TIME val countDownLatch = CountDownLatch(1) val timerReceiver: Any = object : Any() { - @Subscribe - fun sleepTimerUpdate(event: SleepTimerUpdatedEvent?) { + private fun procFlowEvents() { + scope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.SleepTimerUpdatedEvent -> sleepTimerUpdate(event) + else -> {} + } + } + } + } + + fun sleepTimerUpdate(event: FlowEvent.SleepTimerUpdatedEvent?) { if (countDownLatch.count == 0L) { Assert.fail() } countDownLatch.countDown() } } - EventBus.getDefault().register(timerReceiver) +// EventBus.getDefault().register(timerReceiver) val pstm = PlaybackServiceTaskManager(c, defaultPSTM) pstm.setSleepTimer(TIME) countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS) - EventBus.getDefault().unregister(timerReceiver) +// EventBus.getDefault().unregister(timerReceiver) pstm.shutdown() } @@ -230,8 +246,17 @@ class PlaybackServiceTaskManagerTest { val TIMEOUT = 2 * TIME val countDownLatch = CountDownLatch(1) val timerReceiver: Any = object : Any() { - @Subscribe - fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) { + private fun procFlowEvents() { + scope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.SleepTimerUpdatedEvent -> sleepTimerUpdate(event) + else -> {} + } + } + } + } + fun sleepTimerUpdate(event: FlowEvent.SleepTimerUpdatedEvent) { when { event.isOver -> { countDownLatch.countDown() @@ -243,12 +268,12 @@ class PlaybackServiceTaskManagerTest { } } val pstm = PlaybackServiceTaskManager(c, defaultPSTM) - EventBus.getDefault().register(timerReceiver) +// EventBus.getDefault().register(timerReceiver) pstm.setSleepTimer(TIME) pstm.disableSleepTimer() Assert.assertFalse(countDownLatch.await(TIMEOUT, TimeUnit.MILLISECONDS)) pstm.shutdown() - EventBus.getDefault().unregister(timerReceiver) +// EventBus.getDefault().unregister(timerReceiver) } @Test diff --git a/app/src/androidTest/java/ac/test/podcini/ui/UITestUtils.kt b/app/src/androidTest/java/ac/test/podcini/ui/UITestUtils.kt index 143f67ec..f4319555 100644 --- a/app/src/androidTest/java/ac/test/podcini/ui/UITestUtils.kt +++ b/app/src/androidTest/java/ac/test/podcini/ui/UITestUtils.kt @@ -1,21 +1,20 @@ package de.test.podcini.ui -import android.content.Context -import android.util.Log -import androidx.test.platform.app.InstrumentationRegistry -import ac.mdiq.podcini.util.event.FeedListUpdateEvent -import ac.mdiq.podcini.util.event.QueueEvent.Companion.setQueue +import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase +import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance import ac.mdiq.podcini.storage.model.feed.Feed import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedMedia -import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase -import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent +import android.content.Context +import android.util.Log +import androidx.test.platform.app.InstrumentationRegistry import de.test.podcini.util.service.download.HTTPBin import de.test.podcini.util.syndication.feedgenerator.Rss2Generator import org.apache.commons.io.FileUtils import org.apache.commons.io.IOUtils import org.apache.commons.lang3.StringUtils -import org.greenrobot.eventbus.EventBus import org.junit.Assert import java.io.File import java.io.FileOutputStream @@ -193,8 +192,8 @@ class UITestUtils(private val context: Context) { adapter.setCompleteFeed(*hostedFeeds.toTypedArray()) adapter.setQueue(queue) adapter.close() - EventBus.getDefault().post(FeedListUpdateEvent(hostedFeeds)) - EventBus.getDefault().post(setQueue(queue)) + EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(hostedFeeds)) + EventFlow.postEvent(FlowEvent.QueueEvent.setQueue(queue)) } fun setMediaFileName(filename: String) { diff --git a/app/src/androidTest/java/ac/test/podcini/util/event/FeedItemEventListener.kt b/app/src/androidTest/java/ac/test/podcini/util/event/FeedItemEventListener.kt deleted file mode 100644 index bd3a8b6e..00000000 --- a/app/src/androidTest/java/ac/test/podcini/util/event/FeedItemEventListener.kt +++ /dev/null @@ -1,41 +0,0 @@ -package de.test.podcini.util.event - -import ac.mdiq.podcini.util.event.FeedItemEvent -import io.reactivex.functions.Consumer -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe - -/** - * Test helpers to listen [FeedItemEvent] and handle them accordingly - * - */ -class FeedItemEventListener { - private val events: MutableList = ArrayList() - - @Subscribe - fun onEvent(event: FeedItemEvent) { - events.add(event) - } - - fun getEvents(): List { - return events - } - - companion object { - /** - * Provides an listener subscribing to [FeedItemEvent] that the callers can use - * - * Note: it uses RxJava's version of [Consumer] because it allows exceptions to be thrown. - */ - @Throws(Exception::class) - fun withFeedItemEventListener(consumer: Consumer) { - val feedItemEventListener = FeedItemEventListener() - try { - EventBus.getDefault().register(feedItemEventListener) - consumer.accept(feedItemEventListener) - } finally { - EventBus.getDefault().unregister(feedItemEventListener) - } - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/PodciniApp.kt b/app/src/main/java/ac/mdiq/podcini/PodciniApp.kt index 111b7ddf..06a36eaf 100644 --- a/app/src/main/java/ac/mdiq/podcini/PodciniApp.kt +++ b/app/src/main/java/ac/mdiq/podcini/PodciniApp.kt @@ -1,5 +1,13 @@ package ac.mdiq.podcini +import ac.mdiq.podcini.preferences.PreferenceUpgrader +import ac.mdiq.podcini.ui.activity.SplashActivity +import ac.mdiq.podcini.util.SPAUtil +import ac.mdiq.podcini.util.config.ApplicationCallbacksImpl +import ac.mdiq.podcini.util.config.ClientConfig +import ac.mdiq.podcini.util.config.ClientConfigurator +import ac.mdiq.podcini.util.error.CrashReportWriter +import ac.mdiq.podcini.util.error.RxJavaErrorHandlerSetup import android.app.Application import android.content.ComponentName import android.content.Intent @@ -9,15 +17,6 @@ import com.google.android.material.color.DynamicColors import com.joanzapata.iconify.Iconify import com.joanzapata.iconify.fonts.FontAwesomeModule import com.joanzapata.iconify.fonts.MaterialModule -import ac.mdiq.podcini.ui.activity.SplashActivity -import ac.mdiq.podcini.util.config.ApplicationCallbacksImpl -import ac.mdiq.podcini.util.config.ClientConfig -import ac.mdiq.podcini.util.config.ClientConfigurator -import ac.mdiq.podcini.util.error.CrashReportWriter -import ac.mdiq.podcini.util.error.RxJavaErrorHandlerSetup -import ac.mdiq.podcini.preferences.PreferenceUpgrader -import ac.mdiq.podcini.util.SPAUtil -import org.greenrobot.eventbus.EventBus /** Main application class. */ class PodciniApp : Application() { @@ -50,12 +49,12 @@ class PodciniApp : Application() { Iconify.with(MaterialModule()) SPAUtil.sendSPAppsQueryFeedsIntent(this) - EventBus.builder() - .addIndex(ApEventBusIndex()) -// .addIndex(ApCoreEventBusIndex()) - .logNoSubscriberMessages(false) - .sendNoSubscriberEvent(false) - .installDefaultEventBus() +// EventBus.builder() +// .addIndex(ApEventBusIndex()) +//// .addIndex(ApCoreEventBusIndex()) +// .logNoSubscriberMessages(false) +// .sendNoSubscriberEvent(false) +// .installDefaultEventBus() DynamicColors.applyToActivitiesIfAvailable(this) } diff --git a/app/src/main/java/ac/mdiq/podcini/feed/FeedEvent.kt b/app/src/main/java/ac/mdiq/podcini/feed/FeedEvent.kt deleted file mode 100644 index 8c47e3bd..00000000 --- a/app/src/main/java/ac/mdiq/podcini/feed/FeedEvent.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ac.mdiq.podcini.feed - -import org.apache.commons.lang3.builder.ToStringBuilder -import org.apache.commons.lang3.builder.ToStringStyle - -class FeedEvent(private val action: Action, @JvmField val feedId: Long) { - enum class Action { - FILTER_CHANGED, - SORT_ORDER_CHANGED - } - - override fun toString(): String { - return ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) - .append("action", action) - .append("feedId", feedId) - .toString() - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/net/discovery/CombinedSearcher.kt b/app/src/main/java/ac/mdiq/podcini/net/discovery/CombinedSearcher.kt index a1d756b7..adb3ca02 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/discovery/CombinedSearcher.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/discovery/CombinedSearcher.kt @@ -12,10 +12,11 @@ import java.util.* import java.util.concurrent.CountDownLatch class CombinedSearcher : PodcastSearcher { + override fun search(query: String): Single?> { val disposables = ArrayList() - val singleResults: MutableList?> = ArrayList( - Collections.nCopies?>(PodcastSearcherRegistry.searchProviders.size, null)) + val singleResults: MutableList?> = + ArrayList(Collections.nCopies?>(PodcastSearcherRegistry.searchProviders.size, null)) val latch = CountDownLatch(PodcastSearcherRegistry.searchProviders.size) for (i in PodcastSearcherRegistry.searchProviders.indices) { val searchProviderInfo = PodcastSearcherRegistry.searchProviders[i] diff --git a/app/src/main/java/ac/mdiq/podcini/net/download/FeedUpdateManager.kt b/app/src/main/java/ac/mdiq/podcini/net/download/FeedUpdateManager.kt index 96dc2d2f..031319ec 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/download/FeedUpdateManager.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/download/FeedUpdateManager.kt @@ -1,22 +1,21 @@ package ac.mdiq.podcini.net.download import ac.mdiq.podcini.R -import android.content.Context -import android.content.DialogInterface -import android.util.Log -import androidx.work.* -import androidx.work.Constraints.Builder -import com.google.android.material.dialog.MaterialAlertDialogBuilder import ac.mdiq.podcini.net.download.service.FeedUpdateWorker +import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.storage.model.feed.Feed +import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.NetworkUtils.isFeedRefreshAllowed import ac.mdiq.podcini.util.NetworkUtils.isNetworkRestricted import ac.mdiq.podcini.util.NetworkUtils.isVpnOverWifi import ac.mdiq.podcini.util.NetworkUtils.networkAvailable -import ac.mdiq.podcini.util.event.MessageEvent -import ac.mdiq.podcini.storage.model.feed.Feed -import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.util.Logd -import org.greenrobot.eventbus.EventBus +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent +import android.content.Context +import android.content.DialogInterface +import androidx.work.* +import androidx.work.Constraints.Builder +import com.google.android.material.dialog.MaterialAlertDialogBuilder import java.util.concurrent.TimeUnit object FeedUpdateManager { @@ -72,7 +71,7 @@ object FeedUpdateManager { Logd(TAG, "Run auto update immediately in background.") when { feed != null && feed.isLocalFeed -> runOnce(context, feed) - !networkAvailable() -> EventBus.getDefault().post(MessageEvent(context.getString(R.string.download_error_no_connection))) + !networkAvailable() -> EventFlow.postEvent(FlowEvent.MessageEvent(context.getString(R.string.download_error_no_connection))) isFeedRefreshAllowed -> runOnce(context, feed) else -> confirmMobileRefresh(context, feed) } diff --git a/app/src/main/java/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/java/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index 1cf768bb..6ebf8fd9 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -1,17 +1,18 @@ package ac.mdiq.podcini.net.download.service -import android.content.Context -import androidx.work.* -import androidx.work.Constraints.Builder +import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedMedia -import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface -import ac.mdiq.podcini.preferences.UserPreferences +import android.content.Context import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi -import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers +import androidx.work.* +import androidx.work.Constraints.Builder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.concurrent.Future import java.util.concurrent.TimeUnit @@ -39,21 +40,36 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { DBWriter.deleteFeedMediaOfItem(context, media.id) // Remove partially downloaded file val tag = WORK_TAG_EPISODE_URL + media.download_url val future: Future> = WorkManager.getInstance(context).getWorkInfosByTag(tag) - Observable.fromFuture(future) - .subscribeOn(Schedulers.io()) - .observeOn(Schedulers.io()) - .subscribe( - { workInfos: List -> - for (info in workInfos) { - if (info.tags.contains(WORK_DATA_WAS_QUEUED)) { - if (media.item != null) DBWriter.removeQueueItem(context, false, media.item!!) - } +// Observable.fromFuture(future) +// .subscribeOn(Schedulers.io()) +// .observeOn(Schedulers.io()) +// .subscribe( +// { workInfos: List -> +// for (info in workInfos) { +// if (info.tags.contains(WORK_DATA_WAS_QUEUED)) { +// if (media.item != null) DBWriter.removeQueueItem(context, false, media.item!!) +// } +// } +// WorkManager.getInstance(context).cancelAllWorkByTag(tag) +// }, { exception: Throwable -> +// WorkManager.getInstance(context).cancelAllWorkByTag(tag) +// exception.printStackTrace() +// }) + + CoroutineScope(Dispatchers.IO).launch { + try { + val workInfoList = future.get() // Wait for the completion of the future operation and retrieve the result + workInfoList.forEach { workInfo -> + if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) { + if (media.item != null) DBWriter.removeQueueItem(context, false, media.item!!) } - WorkManager.getInstance(context).cancelAllWorkByTag(tag) - }, { exception: Throwable -> - WorkManager.getInstance(context).cancelAllWorkByTag(tag) - exception.printStackTrace() - }) + } + WorkManager.getInstance(context).cancelAllWorkByTag(tag) + } catch (exception: Throwable) { + WorkManager.getInstance(context).cancelAllWorkByTag(tag) + exception.printStackTrace() + } + } } override fun cancelAll(context: Context) { diff --git a/app/src/main/java/ac/mdiq/podcini/net/download/service/EpisodeDownloadWorker.kt b/app/src/main/java/ac/mdiq/podcini/net/download/service/EpisodeDownloadWorker.kt index a7965292..2e43eb84 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/download/service/EpisodeDownloadWorker.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/download/service/EpisodeDownloadWorker.kt @@ -13,7 +13,8 @@ import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter import ac.mdiq.podcini.ui.utils.NotificationUtils import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.config.ClientConfigurator -import ac.mdiq.podcini.util.event.MessageEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Notification import android.app.NotificationManager import android.app.PendingIntent @@ -29,7 +30,6 @@ import androidx.work.WorkerParameters import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import org.apache.commons.io.FileUtils -import org.greenrobot.eventbus.EventBus import java.io.File import java.io.IOException import java.util.* @@ -179,7 +179,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker val retrying = !isLastRunAttempt && !isImmediateFail if (episodeTitle.length > 20) episodeTitle = episodeTitle.substring(0, 19) + "…" - EventBus.getDefault().post(MessageEvent(applicationContext.getString( + EventFlow.postEvent(FlowEvent.MessageEvent(applicationContext.getString( if (retrying) R.string.download_error_retrying else R.string.download_error_not_retrying, episodeTitle), { ctx: Context -> MainActivityStarter(ctx).withDownloadLogsOpen().start() }, applicationContext.getString(R.string.download_error_details))) } @@ -197,10 +197,11 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker } private fun sendErrorNotification(title: String) { - if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent::class.java)) { - sendMessage(title, false) - return - } +// TODO: need to get number of subscribers in SharedFlow +// if (EventBus.getDefault().hasSubscriberForEvent(FlowEvent.MessageEvent::class.java)) { +// sendMessage(title, false) +// return +// } val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR) builder.setTicker(applicationContext.getString(R.string.download_report_title)) diff --git a/app/src/main/java/ac/mdiq/podcini/net/download/service/handler/MediaDownloadedHandler.kt b/app/src/main/java/ac/mdiq/podcini/net/download/service/handler/MediaDownloadedHandler.kt index 787faf3b..dc990db6 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/download/service/handler/MediaDownloadedHandler.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/download/service/handler/MediaDownloadedHandler.kt @@ -10,12 +10,12 @@ import ac.mdiq.podcini.storage.model.download.DownloadError import ac.mdiq.podcini.storage.model.download.DownloadResult import ac.mdiq.podcini.util.ChapterUtils import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.media.MediaMetadataRetriever import android.util.Log import androidx.media3.common.util.UnstableApi -import org.greenrobot.eventbus.EventBus import java.io.File import java.util.concurrent.ExecutionException @@ -69,7 +69,7 @@ class MediaDownloadedHandler(private val context: Context, var updatedStatus: Do // so we do it after the enclosing media has been updated above, // to ensure subscribers will get the updated FeedMedia as well DBWriter.persistFeedItem(item).get() - if (broadcastUnreadStateUpdate) EventBus.getDefault().post(UnreadItemsUpdateEvent()) + if (broadcastUnreadStateUpdate) EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) } } catch (e: InterruptedException) { Log.e(TAG, "MediaHandlerThread was interrupted") diff --git a/app/src/main/java/ac/mdiq/podcini/net/sync/LockingAsyncExecutor.kt b/app/src/main/java/ac/mdiq/podcini/net/sync/LockingAsyncExecutor.kt index 2d38124e..cfcd61b3 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/sync/LockingAsyncExecutor.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/sync/LockingAsyncExecutor.kt @@ -2,6 +2,10 @@ package ac.mdiq.podcini.net.sync import io.reactivex.Completable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.concurrent.locks.ReentrantLock object LockingAsyncExecutor { @@ -21,14 +25,26 @@ object LockingAsyncExecutor { lock.unlock() } } else { - Completable.fromRunnable { - lock.lock() - try { - runnable.run() - } finally { - lock.unlock() +// Completable.fromRunnable { +// lock.lock() +// try { +// runnable.run() +// } finally { +// lock.unlock() +// } +// }.subscribeOn(Schedulers.io()).subscribe() + + val coroutineScope = CoroutineScope(Dispatchers.Main) + coroutineScope.launch { + withContext(Dispatchers.IO) { + lock.lock() + try { + runnable.run() + } finally { + lock.unlock() + } } - }.subscribeOn(Schedulers.io()).subscribe() + } } } } diff --git a/app/src/main/java/ac/mdiq/podcini/net/sync/SyncService.kt b/app/src/main/java/ac/mdiq/podcini/net/sync/SyncService.kt index 3ff56a88..6fcecb2b 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/sync/SyncService.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/sync/SyncService.kt @@ -31,9 +31,7 @@ import ac.mdiq.podcini.ui.utils.NotificationUtils import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.LongList -import ac.mdiq.podcini.util.event.FeedUpdateRunningEvent -import ac.mdiq.podcini.util.event.MessageEvent -import ac.mdiq.podcini.util.event.SyncServiceEvent +import ac.mdiq.podcini.util.event.* import android.app.NotificationManager import android.app.PendingIntent import android.content.Context @@ -44,8 +42,12 @@ import androidx.core.app.NotificationCompat import androidx.media3.common.util.UnstableApi import androidx.work.* import androidx.work.Constraints.Builder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.apache.commons.lang3.StringUtils -import org.greenrobot.eventbus.EventBus import java.util.concurrent.TimeUnit @OptIn(UnstableApi::class) @@ -72,11 +74,11 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont activeSyncProvider.logout() clearErrorNotifications() - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_success)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_success)) SynchronizationSettings.setLastSynchronizationAttemptSuccess(true) return Result.success() } catch (e: Exception) { - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_error)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_error)) SynchronizationSettings.setLastSynchronizationAttemptSuccess(false) Log.e(TAG, Log.getStackTraceString(e)) @@ -97,7 +99,7 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont private fun syncSubscriptions(syncServiceImpl: ISyncService) { Logd(TAG, "syncSubscriptions called") val lastSync = SynchronizationSettings.lastSubscriptionSynchronizationTimestamp - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_subscriptions)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_subscriptions)) val localSubscriptions: List = getFeedListDownloadUrls() val subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync) var newTimeStamp = subscriptionChanges?.timestamp?:0L @@ -151,21 +153,32 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont private fun waitForDownloadServiceCompleted() { Logd(TAG, "waitForDownloadServiceCompleted called") - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_wait_for_downloads)) - try { - while (true) { - Thread.sleep(1000) - val event = EventBus.getDefault().getStickyEvent(FeedUpdateRunningEvent::class.java) - if (event == null || !event.isFeedUpdateRunning) return + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_wait_for_downloads)) + val scope = CoroutineScope(Dispatchers.IO) + scope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.FeedUpdateRunningEvent -> if (!event.isFeedUpdateRunning) return@collectLatest + else -> {} + } } - } catch (e: InterruptedException) { - e.printStackTrace() + return@launch } + scope.cancel() +// try { +// while (true) { +// Thread.sleep(1000) +// val event = EventBus.getDefault().getStickyEvent(FlowEvent.FeedUpdateRunningEvent::class.java) +// if (event == null || !event.isFeedUpdateRunning) return +// } +// } catch (e: InterruptedException) { +// e.printStackTrace() +// } } fun getEpisodeActions(syncServiceImpl: ISyncService) : Pair { val lastSync = SynchronizationSettings.lastEpisodeActionSynchronizationTimestamp - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_episodes_download)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_episodes_download)) val getResponse = syncServiceImpl.getEpisodeActionChanges(lastSync) val newTimeStamp = getResponse?.timestamp?:0L val remoteActions = getResponse?.episodeActions?: listOf() @@ -175,10 +188,10 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont open fun pushEpisodeActions(syncServiceImpl: ISyncService, lastSync: Long, newTimeStamp_: Long): Long { var newTimeStamp = newTimeStamp_ - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_episodes_upload)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_episodes_upload)) val queuedEpisodeActions: MutableList = synchronizationQueueStorage.queuedEpisodeActions if (lastSync == 0L) { - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_upload_played)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_upload_played)) val readItems = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.PLAYED), SortOrder.DATE_NEW_OLD) Logd(TAG, "First sync. Upload state for all " + readItems.size + " played episodes") for (item in readItems) { @@ -272,10 +285,11 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont Logd(TAG, "Skipping sync error notification because of user setting") return } - if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent::class.java)) { - EventBus.getDefault().post(MessageEvent(description)) - return - } +// TODO: +// if (EventBus.getDefault().hasSubscriberForEvent(FlowEvent.MessageEvent::class.java)) { +// EventFlow.postEvent(FlowEvent.MessageEvent(description)) +// return +// } val intent = applicationContext.packageManager.getLaunchIntentForPackage( applicationContext.packageName) @@ -335,7 +349,7 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont } else { // Give it some time, so other possible actions can be queued. builder.setInitialDelay(20L, TimeUnit.SECONDS) - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_started)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_started)) } return builder } diff --git a/app/src/main/java/ac/mdiq/podcini/net/sync/nextcloud/NextcloudLoginFlow.kt b/app/src/main/java/ac/mdiq/podcini/net/sync/nextcloud/NextcloudLoginFlow.kt index 928946b6..5e0b31fa 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/sync/nextcloud/NextcloudLoginFlow.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/sync/nextcloud/NextcloudLoginFlow.kt @@ -1,14 +1,18 @@ package ac.mdiq.podcini.net.sync.nextcloud +import ac.mdiq.podcini.net.sync.HostnameParser import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log -import ac.mdiq.podcini.net.sync.HostnameParser import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request @@ -42,27 +46,51 @@ class NextcloudLoginFlow(private val httpClient: OkHttpClient, private val rawHo poll() return } - startDisposable = Observable.fromCallable { - val url = URI(hostname.scheme, null, hostname.host, hostname.port, hostname.subfolder + "/index.php/login/v2", null, null).toURL() - val result = doRequest(url, "") - val loginUrl = result.getString("login") - this.token = result.getJSONObject("poll").getString("token") - this.endpoint = result.getJSONObject("poll").getString("endpoint") - loginUrl - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { result: String? -> +// startDisposable = Observable.fromCallable { +// val url = URI(hostname.scheme, null, hostname.host, hostname.port, hostname.subfolder + "/index.php/login/v2", null, null).toURL() +// val result = doRequest(url, "") +// val loginUrl = result.getString("login") +// this.token = result.getJSONObject("poll").getString("token") +// this.endpoint = result.getJSONObject("poll").getString("endpoint") +// loginUrl +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe( +// { result: String? -> +// val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(result)) +// context.startActivity(browserIntent) +// poll() +// }, { error: Throwable -> +// Log.e(TAG, Log.getStackTraceString(error)) +// this.token = null +// this.endpoint = null +// callback.onNextcloudAuthError(error.localizedMessage) +// }) + + val coroutineScope = CoroutineScope(Dispatchers.Main) + coroutineScope.launch { + try { + val result = withContext(Dispatchers.IO) { + val url = URI(hostname.scheme, null, hostname.host, hostname.port, hostname.subfolder + "/index.php/login/v2", null, null).toURL() + val result = doRequest(url, "") + val loginUrl = result.getString("login") + token = result.getJSONObject("poll").getString("token") + endpoint = result.getJSONObject("poll").getString("endpoint") + loginUrl + } + withContext(Dispatchers.Main) { val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(result)) context.startActivity(browserIntent) poll() - }, { error: Throwable -> - Log.e(TAG, Log.getStackTraceString(error)) - this.token = null - this.endpoint = null - callback.onNextcloudAuthError(error.localizedMessage) - }) + } + } catch (e: Throwable) { + Log.e(TAG, Log.getStackTraceString(e)) + token = null + endpoint = null + callback.onNextcloudAuthError(e.localizedMessage) + } + } } private fun poll() { diff --git a/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt b/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt index bd643f06..de1d305e 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt @@ -16,7 +16,8 @@ import ac.mdiq.podcini.storage.model.feed.FeedItemFilter import ac.mdiq.podcini.storage.model.feed.SortOrder import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.SyncServiceEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.util.Log import androidx.annotation.OptIn @@ -24,7 +25,6 @@ import androidx.core.content.ContextCompat.getString import androidx.media3.common.util.UnstableApi import androidx.work.* import org.apache.commons.lang3.StringUtils -import org.greenrobot.eventbus.EventBus import org.json.JSONArray import java.io.BufferedReader import java.io.InputStreamReader @@ -53,7 +53,7 @@ import kotlin.math.min val lastSync = SynchronizationSettings.lastEpisodeActionSynchronizationTimestamp val newTimeStamp = pushEpisodeActions(this, 0L, System.currentTimeMillis()) SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp) - EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "50")) + EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "50")) sendToPeer("AllSent", "AllSent") var receivedBye = false @@ -63,8 +63,8 @@ import kotlin.math.min } catch (e: SocketTimeoutException) { Log.e("Guest", getString(context, R.string.sync_error_host_not_respond)) logout() - EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "100")) - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_error, getString(context, R.string.sync_error_host_not_respond))) + EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "100")) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_error, getString(context, R.string.sync_error_host_not_respond))) SynchronizationSettings.setLastSynchronizationAttemptSuccess(false) return Result.failure() } @@ -77,13 +77,13 @@ import kotlin.math.min } catch (e: SocketTimeoutException) { Log.e("Host", getString(context, R.string.sync_error_guest_not_respond)) logout() - EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "100")) - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_error, getString(context, R.string.sync_error_guest_not_respond))) + EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "100")) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_error, getString(context, R.string.sync_error_guest_not_respond))) SynchronizationSettings.setLastSynchronizationAttemptSuccess(false) return Result.failure() } } - EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "50")) + EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "50")) // TODO: not using lastSync val lastSync = SynchronizationSettings.lastEpisodeActionSynchronizationTimestamp val newTimeStamp = pushEpisodeActions(this, 0L, System.currentTimeMillis()) @@ -92,15 +92,15 @@ import kotlin.math.min } } else { logout() - EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "100")) - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_error, "Login failure")) + EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "100")) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_error, "Login failure")) SynchronizationSettings.setLastSynchronizationAttemptSuccess(false) return Result.failure() } logout() - EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "100")) - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_success)) + EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "100")) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_success)) SynchronizationSettings.setLastSynchronizationAttemptSuccess(true) return Result.success() } @@ -109,7 +109,7 @@ import kotlin.math.min @OptIn(UnstableApi::class) override fun login() { Logd(TAG, "serverIp: $hostIp serverPort: $hostPort $isGuest") - EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "2")) + EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "2")) if (!isPortInUse(hostPort)) { if (isGuest) { val maxTries = 120 @@ -159,7 +159,7 @@ import kotlin.math.min Log.w(TAG, "port $hostPort in use, ignored") loginFail = true } - EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "5")) + EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "5")) } @OptIn(UnstableApi::class) private fun isPortInUse(port: Int): Boolean { @@ -236,12 +236,12 @@ import kotlin.math.min override fun pushEpisodeActions(syncServiceImpl: ISyncService, lastSync: Long, newTimeStamp_: Long): Long { var newTimeStamp = newTimeStamp_ - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_episodes_upload)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_episodes_upload)) val queuedEpisodeActions: MutableList = synchronizationQueueStorage.queuedEpisodeActions Logd(TAG, "pushEpisodeActions queuedEpisodeActions: ${queuedEpisodeActions.size}") if (lastSync == 0L) { - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_upload_played)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_upload_played)) // only push downloaded items val pausedItems = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.PAUSED), SortOrder.DATE_NEW_OLD) val readItems = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.PLAYED), SortOrder.DATE_NEW_OLD) @@ -368,7 +368,7 @@ import kotlin.math.min // Give it some time, so other possible actions can be queued. builder.setInitialDelay(20L, TimeUnit.SECONDS) - EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_started)) + EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_started)) val workRequest: OneTimeWorkRequest = builder.setInitialDelay(0L, TimeUnit.SECONDS).build() WorkManager.getInstance(context).enqueueUniqueWork(hostIp_, ExistingWorkPolicy.REPLACE, workRequest) diff --git a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt index a5e5cd48..2a567f55 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt @@ -13,9 +13,8 @@ import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent -import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent -import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.* import android.os.Build import android.os.IBinder @@ -23,10 +22,10 @@ import android.util.Log import android.util.Pair import android.view.SurfaceHolder import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch /** * Communicates with the playback service. GUI classes should use this class to @@ -64,16 +63,22 @@ abstract class PlaybackController(private val activity: FragmentActivity) { @Synchronized fun init() { if (!eventsRegistered) { - EventBus.getDefault().register(this) + procFlowEvents() eventsRegistered = true } if (PlaybackService.isRunning) initServiceRunning() else updatePlayButtonShowsPlay(true) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackServiceEvent) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_STARTED) init() + private fun procFlowEvents() { + activity.lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.PlaybackServiceEvent -> if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_STARTED) init() + else -> {} + } + } + } } @Synchronized @@ -118,7 +123,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { released = true if (eventsRegistered) { - EventBus.getDefault().unregister(this) + eventsRegistered = false } } @@ -314,7 +319,8 @@ abstract class PlaybackController(private val activity: FragmentActivity) { if (media is FeedMedia) { media!!.setPosition(time) DBWriter.persistFeedItem((media as FeedMedia).item) - EventBus.getDefault().post(PlaybackPositionEvent(time, media!!.getDuration())) + EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(time, media!!.getDuration())) +// EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(time, media!!.getDuration())) } } } @@ -384,7 +390,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) { if (playbackService != null) playbackService!!.setSpeed(speed, codeArray) else { UserPreferences.setPlaybackSpeed(speed) - EventBus.getDefault().post(SpeedChangedEvent(speed)) + EventFlow.postEvent(FlowEvent.SpeedChangedEvent(speed)) } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt index 6607f3af..aa9f8299 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt @@ -14,9 +14,8 @@ import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.NetworkUtils.wasDownloadBlocked import ac.mdiq.podcini.util.config.ClientConfig -import ac.mdiq.podcini.util.event.PlayerErrorEvent -import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent -import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.UiModeManager import android.content.Context import android.content.res.Configuration @@ -49,7 +48,6 @@ import androidx.media3.extractor.mp3.Mp3Extractor import androidx.media3.ui.DefaultTrackNameProvider import androidx.media3.ui.TrackNameProvider import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus import java.io.File import java.io.IOException import java.lang.Runnable @@ -87,6 +85,8 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP private var mediaSource: MediaSource? = null private var playbackParameters: PlaybackParameters + private var bufferedPercentagePrev = 0 + private val formats: List get() { val formats: MutableList = arrayListOf() @@ -297,11 +297,11 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP } catch (e: IOException) { e.printStackTrace() setPlayerStatus(PlayerStatus.ERROR, null) - EventBus.getDefault().postSticky(PlayerErrorEvent(e.localizedMessage ?: "")) + EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: "")) } catch (e: IllegalStateException) { e.printStackTrace() setPlayerStatus(PlayerStatus.ERROR, null) - EventBus.getDefault().postSticky(PlayerErrorEvent(e.localizedMessage ?: "")) + EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: "")) } } @@ -509,7 +509,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP * This method is executed on an internal executor service. */ override fun setPlaybackParams(speed: Float, skipSilence: Boolean) { - EventBus.getDefault().post(SpeedChangedEvent(speed)) + EventFlow.postEvent(FlowEvent.SpeedChangedEvent(speed)) Logd(TAG, "setPlaybackParams speed=$speed pitch=${playbackParameters.pitch} skipSilence=$skipSilence") playbackParameters = PlaybackParameters(speed, playbackParameters.pitch) exoPlayer!!.skipSilenceEnabled = skipSilence @@ -679,7 +679,10 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP while (true) { delay(bufferUpdateInterval) withContext(Dispatchers.Main) { - bufferingUpdateListener?.accept(exoPlayer!!.bufferedPercentage) + if (bufferedPercentagePrev != exoPlayer!!.bufferedPercentage) { + bufferingUpdateListener?.accept(exoPlayer!!.bufferedPercentage) + bufferedPercentagePrev = exoPlayer!!.bufferedPercentage + } } } } @@ -754,14 +757,14 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP audioSeekCompleteListener = Runnable { this.genericSeekCompleteListener() } bufferingUpdateListener = Consumer { percent: Int -> when (percent) { - BUFFERING_STARTED -> EventBus.getDefault().post(BufferUpdateEvent.started()) - BUFFERING_ENDED -> EventBus.getDefault().post(BufferUpdateEvent.ended()) - else -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent)) + BUFFERING_STARTED -> EventFlow.postEvent(FlowEvent.BufferUpdateEvent.started()) + BUFFERING_ENDED -> EventFlow.postEvent(FlowEvent.BufferUpdateEvent.ended()) + else -> EventFlow.postEvent(FlowEvent.BufferUpdateEvent.progressUpdate(0.01f * percent)) } } audioErrorListener = Consumer { message: String -> Log.e(TAG, "PlayerErrorEvent: $message") - EventBus.getDefault().postSticky(PlayerErrorEvent(message)) + EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(message)) } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt index eb57e70b..780b19dc 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -57,12 +57,8 @@ import ac.mdiq.podcini.util.FeedUtil.shouldAutoDeleteItemsOnThatFeed import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed -import ac.mdiq.podcini.util.event.MessageEvent -import ac.mdiq.podcini.util.event.PlayerErrorEvent -import ac.mdiq.podcini.util.event.playback.* -import ac.mdiq.podcini.util.event.settings.SkipIntroEndingChangedEvent -import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent -import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.annotation.SuppressLint import android.app.NotificationManager import android.app.PendingIntent @@ -93,11 +89,9 @@ import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import java.util.* import kotlin.concurrent.Volatile import kotlin.math.max @@ -197,7 +191,7 @@ class PlaybackService : MediaSessionService() { registerReceiver(headsetDisconnected, IntentFilter(Intent.ACTION_HEADSET_PLUG)) registerReceiver(bluetoothStateUpdated, IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) registerReceiver(audioBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) - EventBus.getDefault().register(this) + procFlowEvents() taskManager = PlaybackServiceTaskManager(this, taskManagerCallback) recreateMediaSessionIfNeeded() @@ -207,7 +201,7 @@ class PlaybackService : MediaSessionService() { recreateMediaPlayer() } } - EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_STARTED)) + EventFlow.postEvent(FlowEvent.PlaybackServiceEvent(FlowEvent.PlaybackServiceEvent.Action.SERVICE_STARTED)) } fun recreateMediaSessionIfNeeded() { @@ -277,7 +271,7 @@ class PlaybackService : MediaSessionService() { unregisterReceiver(bluetoothStateUpdated) unregisterReceiver(audioBecomingNoisy) taskManager.shutdown() - EventBus.getDefault().unregister(this) + } fun isServiceReady(): Boolean { @@ -469,10 +463,11 @@ class PlaybackService : MediaSessionService() { @SuppressLint("LaunchActivityFromNotification") private fun displayStreamingNotAllowedNotification(originalIntent: Intent) { - if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent::class.java)) { - EventBus.getDefault().post(MessageEvent(getString(R.string.confirm_mobile_streaming_notification_message))) - return - } +// TODO +// if (EventBus.getDefault().hasSubscriberForEvent(FlowEvent.MessageEvent::class.java)) { +// EventFlow.postEvent(FlowEvent.MessageEvent(getString(R.string.confirm_mobile_streaming_notification_message))) +// return +// } val intentAllowThisTime = Intent(originalIntent) intentAllowThisTime.setAction(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_THIS_TIME) @@ -666,7 +661,8 @@ class PlaybackService : MediaSessionService() { override fun positionSaverTick() { if (currentPosition != previousPosition) { // Log.d(TAG, "positionSaverTick currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed") - EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration)) + EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(currentPosition, duration)) +// EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(currentPosition, duration)) skipEndingIfNecessary() saveCurrentPosition(true, null, Playable.INVALID_TIME) previousPosition = currentPosition @@ -718,7 +714,7 @@ class PlaybackService : MediaSessionService() { if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING && autoEnable() && autoEnableByTime && !sleepTimerActive()) { setSleepTimer(timerMillis()) - EventBus.getDefault().post(MessageEvent(getString(R.string.sleep_timer_enabled_label), { disableSleepTimer() }, getString(R.string.undo))) + EventFlow.postEvent(FlowEvent.MessageEvent(getString(R.string.sleep_timer_enabled_label), { disableSleepTimer() }, getString(R.string.undo))) } // loadQueueForMediaSession() } @@ -784,16 +780,29 @@ class PlaybackService : MediaSessionService() { } } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun playerError(event: PlayerErrorEvent?) { + private fun procFlowEvents() { + scope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.PlayerErrorEvent -> playerError(event) + is FlowEvent.BufferUpdateEvent -> bufferUpdate(event) + is FlowEvent.SleepTimerUpdatedEvent -> sleepTimerUpdate(event) + is FlowEvent.VolumeAdaptionChangedEvent -> volumeAdaptionChanged(event) + is FlowEvent.SpeedPresetChangedEvent -> onSpeedPresetChanged(event) + is FlowEvent.SkipIntroEndingChangedEvent -> skipIntroEndingPresetChanged(event) + is FlowEvent.StartPlayEvent -> currentitem = event.item + else -> {} + } + } + } + } + + fun playerError(event: FlowEvent.PlayerErrorEvent) { if (MediaPlayerBase.status == PlayerStatus.PLAYING || MediaPlayerBase.status == PlayerStatus.FALLBACK) mediaPlayer!!.pause(abandonFocus = true, reinit = false) } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun bufferUpdate(event: BufferUpdateEvent) { + fun bufferUpdate(event: FlowEvent.BufferUpdateEvent) { if (event.hasEnded()) { val playable = playable if (this.playable is FeedMedia && playable!!.getDuration() <= 0 && (mediaPlayer?.getDuration()?:0) > 0) { @@ -804,9 +813,7 @@ class PlaybackService : MediaSessionService() { } } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) { + fun sleepTimerUpdate(event: FlowEvent.SleepTimerUpdatedEvent) { when { event.isOver -> { mediaPlayer?.pause(abandonFocus = true, reinit = true) @@ -856,7 +863,7 @@ class PlaybackService : MediaSessionService() { writeNoMediaPlaying() return null } - EventBus.getDefault().post(StartPlayEvent(nextItem)) + EventFlow.postEvent(FlowEvent.StartPlayEvent(nextItem)) return nextItem.media } @@ -1157,20 +1164,16 @@ class PlaybackService : MediaSessionService() { private val shutdownReceiver: BroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (TextUtils.equals(intent.action, PlaybackServiceConstants.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) - EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)) + EventFlow.postEvent(FlowEvent.PlaybackServiceEvent(FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)) } } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun volumeAdaptionChanged(event: VolumeAdaptionChangedEvent) { + fun volumeAdaptionChanged(event: FlowEvent.VolumeAdaptionChangedEvent) { val playbackVolumeUpdater = PlaybackVolumeUpdater() if (mediaPlayer != null) playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer!!, event.feedId, event.volumeAdaptionSetting) } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun onSpeedPresetChanged(event: SpeedPresetChangedEvent) { + fun onSpeedPresetChanged(event: FlowEvent.SpeedPresetChangedEvent) { val item = (playable as? FeedMedia)?.item ?: currentitem if (item?.feed?.id == event.feedId) { if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) setSpeed(getPlaybackSpeed(playable!!.getMediaType())) @@ -1178,9 +1181,7 @@ class PlaybackService : MediaSessionService() { } } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun skipIntroEndingPresetChanged(event: SkipIntroEndingChangedEvent) { + fun skipIntroEndingPresetChanged(event: FlowEvent.SkipIntroEndingChangedEvent) { val item = (playable as? FeedMedia)?.item ?: currentitem // if (playable is FeedMedia) { if (item?.feed?.id == event.feedId) { @@ -1194,12 +1195,6 @@ class PlaybackService : MediaSessionService() { // } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEvenStartPlay(event: StartPlayEvent) { - Logd(TAG, "onEvenStartPlay ${event.item.title}") - currentitem = event.item - } - fun resume() { mediaPlayer?.resume() taskManager.restartSleepTimer() @@ -1244,7 +1239,7 @@ class PlaybackService : MediaSessionService() { feedPreferences.feedPlaybackSpeed = speed Logd(TAG, "setSpeed ${feed.title} $speed") DBWriter.persistFeedPreferences(feedPreferences) - EventBus.getDefault().post(SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id)) + EventFlow.postEvent(FlowEvent.SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id)) } } } @@ -1288,7 +1283,8 @@ class PlaybackService : MediaSessionService() { fun seekTo(t: Int) { mediaPlayer?.seekTo(t) - EventBus.getDefault().post(PlaybackPositionEvent(t, duration)) + EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(t, duration)) +// EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(t, duration)) } fun setAudioTrack(track: Int) { diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt index 561d849e..4fcd1a46 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt @@ -6,7 +6,8 @@ import ac.mdiq.podcini.ui.widget.WidgetUpdater import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState import ac.mdiq.podcini.util.ChapterUtils import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.playback.SleepTimerUpdatedEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.os.Handler import android.os.Looper @@ -16,7 +17,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.greenrobot.eventbus.EventBus import java.util.concurrent.ScheduledFuture import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit @@ -149,7 +149,7 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb if (isSleepTimerActive) sleepTimerFuture!!.cancel(true) sleepTimer = SleepTimer(waitingTime) sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS) - EventBus.getDefault().post(SleepTimerUpdatedEvent.justEnabled(waitingTime)) + EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.justEnabled(waitingTime)) } /** @@ -263,7 +263,7 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb override fun run() { Logd(TAG, "Starting SleepTimer") var lastTick = System.currentTimeMillis() - EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(timeLeft)) + EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.updated(timeLeft)) while (timeLeft > 0) { try { Thread.sleep(UPDATE_INTERVAL) @@ -277,7 +277,7 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb timeLeft -= now - lastTick lastTick = now - EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(timeLeft)) + EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.updated(timeLeft)) if (timeLeft < NOTIFICATION_THRESHOLD) { Logd(TAG, "Sleep timer is about to expire") if (SleepTimerPreferences.vibrate() && !hasVibrated) { @@ -304,7 +304,7 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb } fun restart() { - EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled()) + EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.cancelled()) setSleepTimer(waitingTime) shakeListener?.pause() shakeListener = null @@ -314,7 +314,7 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb sleepTimerFuture!!.cancel(true) shakeListener?.pause() - EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled()) + EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.cancelled()) } } diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/PlaybackPreferences.kt b/app/src/main/java/ac/mdiq/podcini/preferences/PlaybackPreferences.kt index 7af03f16..db3e25d5 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/PlaybackPreferences.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/PlaybackPreferences.kt @@ -8,13 +8,13 @@ import ac.mdiq.podcini.storage.model.feed.FeedPreferences import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.PlayerStatusEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.content.SharedPreferences import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.util.Log import androidx.preference.PreferenceManager -import org.greenrobot.eventbus.EventBus /** * Provides access to preferences set by the playback service. A private @@ -24,7 +24,7 @@ import org.greenrobot.eventbus.EventBus class PlaybackPreferences private constructor() : OnSharedPreferenceChangeListener { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { - if (PREF_CURRENT_PLAYER_STATUS == key) EventBus.getDefault().post(PlayerStatusEvent()) + if (PREF_CURRENT_PLAYER_STATUS == key) EventFlow.postEvent(FlowEvent.PlayerStatusEvent()) } companion object { diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/java/ac/mdiq/podcini/preferences/UserPreferences.kt index 925979c5..e5d064b8 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -56,6 +56,7 @@ object UserPreferences { const val PREF_QUEUE_KEEP_SORTED_ORDER: String = "prefQueueKeepSortedOrder" const val PREF_NEW_EPISODES_ACTION: String = "prefNewEpisodesAction" // not used private const val PREF_DOWNLOADS_SORTED_ORDER = "prefDownloadSortedOrder" + private const val PREF_HISTORY_SORTED_ORDER = "prefHistorySortedOrder" private const val PREF_INBOX_SORTED_ORDER = "prefInboxSortedOrder" // Episode @@ -835,16 +836,23 @@ object UserPreferences { } // @JvmStatic -// var inboxSortedOrder: SortOrder? +// var historySortedOrder: SortOrder? +// /** +// * Returns the sort order for the downloads. +// */ // get() { -// val sortOrderStr = prefs.getString(PREF_INBOX_SORTED_ORDER, "" + SortOrder.DATE_NEW_OLD.code) +// val sortOrderStr = prefs.getString(PREF_HISTORY_SORTED_ORDER, "" + SortOrder.PLAYED_DATE_NEW_OLD.code) // return SortOrder.fromCodeString(sortOrderStr) // } +// /** +// * Sets the sort order for the downloads. +// */ // set(sortOrder) { -// prefs.edit().putString(PREF_INBOX_SORTED_ORDER, "" + sortOrder!!.code).apply() +// prefs.edit().putString(PREF_HISTORY_SORTED_ORDER, "" + sortOrder!!.code).apply() // } -// @JvmStatic + + @JvmStatic var subscriptionsFilter: SubscriptionsFilter get() { val value = prefs.getString(PREF_FILTER_FEED, "") diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt index 3e634c22..c132f94c 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt @@ -2,6 +2,7 @@ package ac.mdiq.podcini.preferences.fragments import ac.mdiq.podcini.PodciniApp.Companion.forceRestart import ac.mdiq.podcini.R +import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.storage.DatabaseTransporter import ac.mdiq.podcini.storage.PreferencesTransporter import ac.mdiq.podcini.storage.asynctask.DocumentFileExportWorker @@ -12,6 +13,8 @@ import ac.mdiq.podcini.storage.export.html.HtmlWriter import ac.mdiq.podcini.storage.export.opml.OpmlWriter import ac.mdiq.podcini.ui.activity.OpmlImportActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity +import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog +import ac.mdiq.podcini.util.Logd import android.app.Activity.RESULT_OK import android.app.ProgressDialog import android.content.ActivityNotFoundException @@ -29,6 +32,7 @@ import androidx.annotation.StringRes import androidx.core.app.ShareCompat.IntentBuilder import androidx.core.content.FileProvider import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -37,6 +41,10 @@ import io.reactivex.Completable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.text.SimpleDateFormat import java.util.* @@ -245,38 +253,80 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { if (result.resultCode != RESULT_OK || result.data == null) return val uri = result.data!!.data progressDialog!!.show() - disposable = Completable.fromAction { DatabaseTransporter.importBackup(uri, requireContext()) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - showDatabaseImportSuccessDialog() - progressDialog!!.dismiss() - }, { error: Throwable -> this.showExportErrorDialog(error) }) +// disposable = Completable.fromAction { DatabaseTransporter.importBackup(uri, requireContext()) } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ +// showDatabaseImportSuccessDialog() +// progressDialog!!.dismiss() +// }, { error: Throwable -> this.showExportErrorDialog(error) }) + + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + DatabaseTransporter.importBackup(uri, requireContext()) + } + withContext(Dispatchers.Main) { + showDatabaseImportSuccessDialog() + progressDialog!!.dismiss() + } + } catch (e: Throwable) { + showExportErrorDialog(e) + } + } } private fun restorePreferencesResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data?.data == null) return val uri = result.data!!.data!! progressDialog!!.show() - disposable = Completable.fromAction { PreferencesTransporter.importBackup(uri, requireContext()) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - showDatabaseImportSuccessDialog() - progressDialog!!.dismiss() - }, { error: Throwable -> this.showExportErrorDialog(error) }) +// disposable = Completable.fromAction { PreferencesTransporter.importBackup(uri, requireContext()) } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ +// showDatabaseImportSuccessDialog() +// progressDialog!!.dismiss() +// }, { error: Throwable -> this.showExportErrorDialog(error) }) + + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + PreferencesTransporter.importBackup(uri, requireContext()) + } + withContext(Dispatchers.Main) { + showDatabaseImportSuccessDialog() + progressDialog!!.dismiss() + } + } catch (e: Throwable) { + showExportErrorDialog(e) + } + } } private fun backupDatabaseResult(uri: Uri?) { if (uri == null) return progressDialog!!.show() - disposable = Completable.fromAction { DatabaseTransporter.exportToDocument(uri, requireContext()) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - showExportSuccessSnackbar(uri, "application/x-sqlite3") - progressDialog!!.dismiss() - }, { error: Throwable -> this.showExportErrorDialog(error) }) +// disposable = Completable.fromAction { DatabaseTransporter.exportToDocument(uri, requireContext()) } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ +// showExportSuccessSnackbar(uri, "application/x-sqlite3") +// progressDialog!!.dismiss() +// }, { error: Throwable -> this.showExportErrorDialog(error) }) + + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + DatabaseTransporter.exportToDocument(uri, requireContext()) + } + withContext(Dispatchers.Main) { + showExportSuccessSnackbar(uri, "application/x-sqlite3") + progressDialog!!.dismiss() + } + } catch (e: Throwable) { + showExportErrorDialog(e) + } + } } private fun chooseOpmlImportPathResult(uri: Uri?) { diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt index 33250a73..d1a12226 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/PlaybackPreferencesFragment.kt @@ -6,7 +6,8 @@ import ac.mdiq.podcini.preferences.UsageStatistics.doNotAskAgain import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.dialog.* -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Activity import android.os.Build import android.os.Bundle @@ -16,7 +17,6 @@ import androidx.media3.common.util.UnstableApi import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import org.greenrobot.eventbus.EventBus class PlaybackPreferencesFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { @@ -61,7 +61,7 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() { } findPreference(PREF_PLAYBACK_PREFER_STREAMING)!!.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _: Preference?, _: Any? -> // Update all visible lists to reflect new streaming action button - EventBus.getDefault().post(UnreadItemsUpdateEvent()) + EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) // User consciously decided whether to prefer the streaming button, disable suggestion to change that doNotAskAgain(UsageStatistics.ACTION_STREAM) true diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt index b021db36..fd98700e 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt @@ -7,8 +7,8 @@ import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting import ac.mdiq.podcini.ui.activity.PreferenceActivity import ac.mdiq.podcini.ui.dialog.DrawerPreferencesDialog import ac.mdiq.podcini.ui.dialog.FeedSortDialog -import ac.mdiq.podcini.util.event.PlayerStatusEvent -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.content.DialogInterface import android.os.Build @@ -19,7 +19,6 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import org.greenrobot.eventbus.EventBus class UserInterfacePreferencesFragment : PreferenceFragmentCompat() { @@ -45,8 +44,8 @@ class UserInterfacePreferencesFragment : PreferenceFragmentCompat() { findPreference(UserPreferences.PREF_SHOW_TIME_LEFT)?.setOnPreferenceChangeListener { _: Preference?, newValue: Any? -> setShowRemainTimeSetting(newValue as Boolean?) - EventBus.getDefault().post(UnreadItemsUpdateEvent()) - EventBus.getDefault().post(PlayerStatusEvent()) + EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) + EventFlow.postEvent(FlowEvent.PlayerStatusEvent()) true } diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/DevelopersFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/DevelopersFragment.kt index 3c57900a..ef0ed02f 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/DevelopersFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/DevelopersFragment.kt @@ -1,47 +1,37 @@ package ac.mdiq.podcini.preferences.fragments.about +import ac.mdiq.podcini.ui.adapter.SimpleIconListAdapter import android.R.color import android.os.Bundle import android.view.View import android.widget.Toast import androidx.fragment.app.ListFragment -import ac.mdiq.podcini.ui.adapter.SimpleIconListAdapter -import io.reactivex.Single -import io.reactivex.SingleEmitter -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.BufferedReader import java.io.InputStreamReader class DevelopersFragment : ListFragment() { - private var developersLoader: Disposable? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) listView.divider = null listView.setSelector(color.transparent) - developersLoader = Single.create { emitter: SingleEmitter?> -> + lifecycleScope.launch(Dispatchers.IO) { val developers = ArrayList() val reader = BufferedReader(InputStreamReader(requireContext().assets.open("developers.csv"), "UTF-8")) - var line: String - while ((reader.readLine().also { line = it }) != null) { + var line = "" + while ((reader.readLine()?.also { line = it }) != null) { val info = line.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() developers.add(SimpleIconListAdapter.ListItem(info[0], info[2], "https://avatars2.githubusercontent.com/u/" + info[1] + "?s=60&v=4")) } - emitter.onSuccess(developers) + withContext(Dispatchers.Main) { + listAdapter = SimpleIconListAdapter(requireContext(), developers) } + }.invokeOnCompletion { throwable -> + if (throwable != null) Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ developers: ArrayList? -> - if (developers != null) listAdapter = SimpleIconListAdapter(requireContext(), developers) - }, { error: Throwable -> Toast.makeText(context, error.message, Toast.LENGTH_LONG).show() } - ) - } - - override fun onStop() { - super.onStop() - developersLoader?.dispose() } } diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/LicensesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/LicensesFragment.kt index 1ec57140..1deae87b 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/LicensesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/LicensesFragment.kt @@ -1,35 +1,33 @@ package ac.mdiq.podcini.preferences.fragments.about +import ac.mdiq.podcini.R +import ac.mdiq.podcini.ui.activity.PreferenceActivity +import ac.mdiq.podcini.ui.adapter.SimpleIconListAdapter +import ac.mdiq.podcini.util.IntentUtils.openInBrowser import android.content.DialogInterface import android.os.Bundle import android.view.View import android.widget.ListView import android.widget.Toast import androidx.fragment.app.ListFragment +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.ui.activity.PreferenceActivity -import ac.mdiq.podcini.ui.adapter.SimpleIconListAdapter -import ac.mdiq.podcini.util.IntentUtils.openInBrowser -import io.reactivex.Single -import io.reactivex.SingleEmitter -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.BufferedReader import java.io.IOException import java.io.InputStreamReader import javax.xml.parsers.DocumentBuilderFactory class LicensesFragment : ListFragment() { - private var licensesLoader: Disposable? = null private val licenses = ArrayList() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) listView.divider = null - licensesLoader = Single.create { emitter: SingleEmitter?> -> + lifecycleScope.launch(Dispatchers.IO) { licenses.clear() val stream = requireContext().assets.open("licenses.xml") val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() @@ -40,14 +38,14 @@ class LicensesFragment : ListFragment() { String.format("By %s, %s license", lib.getNamedItem("author").textContent, lib.getNamedItem("license").textContent), "", lib.getNamedItem("website").textContent, lib.getNamedItem("licenseText").textContent)) } - emitter.onSuccess(licenses) + withContext(Dispatchers.Main) { + listAdapter = SimpleIconListAdapter(requireContext(), licenses) + } + }.invokeOnCompletion { throwable -> + if (throwable!= null) { + Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() + } } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { developers: ArrayList? -> if (developers != null) listAdapter = SimpleIconListAdapter(requireContext(), developers) }, - { error: Throwable -> Toast.makeText(context, error.message, Toast.LENGTH_LONG).show() } - ) } private class LicenseItem(title: String, subtitle: String, imageUrl: String, val licenseUrl: String, val licenseTextFile: String) @@ -72,8 +70,8 @@ class LicensesFragment : ListFragment() { try { val reader = BufferedReader(InputStreamReader(requireContext().assets.open(licenseTextFile), "UTF-8")) val licenseText = StringBuilder() - var line: String? - while ((reader.readLine().also { line = it }) != null) { + var line = "" + while ((reader.readLine()?.also { line = it }) != null) { licenseText.append(line).append("\n") } @@ -85,11 +83,6 @@ class LicensesFragment : ListFragment() { } } - override fun onStop() { - super.onStop() - licensesLoader?.dispose() - } - override fun onStart() { super.onStart() (activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.licenses) diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/SpecialThanksFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/SpecialThanksFragment.kt index a0ab2258..c56a9c68 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/SpecialThanksFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/SpecialThanksFragment.kt @@ -1,47 +1,40 @@ package ac.mdiq.podcini.preferences.fragments.about +import ac.mdiq.podcini.ui.adapter.SimpleIconListAdapter import android.R.color import android.os.Bundle import android.view.View import android.widget.Toast import androidx.fragment.app.ListFragment -import ac.mdiq.podcini.ui.adapter.SimpleIconListAdapter -import io.reactivex.Single -import io.reactivex.SingleEmitter -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.BufferedReader import java.io.InputStreamReader class SpecialThanksFragment : ListFragment() { - private var translatorsLoader: Disposable? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) listView.divider = null listView.setSelector(color.transparent) - translatorsLoader = Single.create { emitter: SingleEmitter?> -> + lifecycleScope.launch(Dispatchers.IO) { val translators = ArrayList() val reader = BufferedReader(InputStreamReader(requireContext().assets.open("special_thanks.csv"), "UTF-8")) - var line: String - while ((reader.readLine().also { line = it }) != null) { + var line = "" + while ((reader.readLine()?.also { line = it }) != null) { val info = line.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() translators.add(SimpleIconListAdapter.ListItem(info[0], info[1], info[2])) } - emitter.onSuccess(translators) + withContext(Dispatchers.Main) { + listAdapter = SimpleIconListAdapter(requireContext(), translators) + } + }.invokeOnCompletion { throwable -> + if (throwable!= null) { + Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() + } } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ translators: ArrayList? -> - if (translators != null) listAdapter = SimpleIconListAdapter(requireContext(), translators) - }, { error: Throwable -> Toast.makeText(context, error.message, Toast.LENGTH_LONG).show() } - ) - } - - override fun onStop() { - super.onStop() - translatorsLoader?.dispose() } } diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/TranslatorsFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/TranslatorsFragment.kt index 73d25a9e..a76622dd 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/TranslatorsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/TranslatorsFragment.kt @@ -6,43 +6,37 @@ import android.view.View import android.widget.Toast import androidx.fragment.app.ListFragment import ac.mdiq.podcini.ui.adapter.SimpleIconListAdapter +import androidx.lifecycle.lifecycleScope import io.reactivex.Single import io.reactivex.SingleEmitter import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.BufferedReader import java.io.InputStreamReader class TranslatorsFragment : ListFragment() { - private var translatorsLoader: Disposable? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) listView.divider = null listView.setSelector(color.transparent) - translatorsLoader = Single.create { emitter: SingleEmitter?> -> + lifecycleScope.launch(Dispatchers.IO) { val translators = ArrayList() val reader = BufferedReader(InputStreamReader(requireContext().assets.open("translators.csv"), "UTF-8")) - var line: String - while ((reader.readLine().also { line = it }) != null) { + var line = "" + while ((reader.readLine()?.also { line = it }) != null) { val info = line.split(";".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() translators.add(SimpleIconListAdapter.ListItem(info[0], info[1], "")) } - emitter.onSuccess(translators) + withContext(Dispatchers.Main) { + listAdapter = SimpleIconListAdapter(requireContext(), translators) } + }.invokeOnCompletion { throwable -> + if (throwable != null) Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ translators: ArrayList? -> - if (translators != null) listAdapter = SimpleIconListAdapter(requireContext(), translators) - }, - { error: Throwable -> Toast.makeText(context, error.message, Toast.LENGTH_LONG).show() } - ) - } - - override fun onStop() { - super.onStop() - translatorsLoader?.dispose() } } diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/GpodderAuthenticationFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/GpodderAuthenticationFragment.kt index 2a05ee9d..3fc8f7f2 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/GpodderAuthenticationFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/GpodderAuthenticationFragment.kt @@ -21,11 +21,11 @@ import android.view.inputmethod.InputMethodManager import android.widget.TextView import android.widget.ViewFlipper import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import com.google.android.material.dialog.MaterialAlertDialogBuilder -import io.reactivex.Completable -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.regex.Pattern import kotlin.concurrent.Volatile @@ -109,25 +109,48 @@ class GpodderAuthenticationFragment : DialogFragment() { txtvError.visibility = View.GONE val inputManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager inputManager.hideSoftInputFromWindow(login.windowToken, InputMethodManager.HIDE_NOT_ALWAYS) - Completable.fromAction { - service?.setCredentials(usernameStr, passwordStr) - service?.login() - if (service != null) devices = service!!.devices - this@GpodderAuthenticationFragment.username = usernameStr - this@GpodderAuthenticationFragment.password = passwordStr - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ + +// Completable.fromAction { +// service?.setCredentials(usernameStr, passwordStr) +// service?.login() +// if (service != null) devices = service!!.devices +// this@GpodderAuthenticationFragment.username = usernameStr +// this@GpodderAuthenticationFragment.password = passwordStr +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ +// login.isEnabled = true +// progressBar.visibility = View.GONE +// advance() +// }, { error: Throwable -> +// login.isEnabled = true +// progressBar.visibility = View.GONE +// txtvError.text = error.cause!!.message +// txtvError.visibility = View.VISIBLE +// }) + + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + service?.setCredentials(usernameStr, passwordStr) + service?.login() + if (service != null) devices = service!!.devices + this@GpodderAuthenticationFragment.username = usernameStr + this@GpodderAuthenticationFragment.password = passwordStr + } + withContext(Dispatchers.Main) { + login.isEnabled = true + progressBar.visibility = View.GONE + advance() + } + } catch (e: Throwable) { login.isEnabled = true progressBar.visibility = View.GONE - advance() - }, { error: Throwable -> - login.isEnabled = true - progressBar.visibility = View.GONE - txtvError.text = error.cause!!.message + txtvError.text = e.cause!!.message txtvError.visibility = View.VISIBLE - }) + } + } } } @@ -166,23 +189,43 @@ class GpodderAuthenticationFragment : DialogFragment() { txtvError.visibility = View.GONE deviceName.isEnabled = false - Observable.fromCallable { - val deviceId = generateDeviceId(deviceNameStr) - service!!.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE) - GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ device: GpodnetDevice? -> - progBarCreateDevice.visibility = View.GONE - selectedDevice = device - advance() - }, { error: Throwable -> +// Observable.fromCallable { +// val deviceId = generateDeviceId(deviceNameStr) +// service!!.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE) +// GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0) +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ device: GpodnetDevice? -> +// progBarCreateDevice.visibility = View.GONE +// selectedDevice = device +// advance() +// }, { error: Throwable -> +// deviceName.isEnabled = true +// progBarCreateDevice.visibility = View.GONE +// txtvError.text = error.message +// txtvError.visibility = View.VISIBLE +// }) + + lifecycleScope.launch { + try { + val device = withContext(Dispatchers.IO) { + val deviceId = generateDeviceId(deviceNameStr) + service!!.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE) + GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0) + } + withContext(Dispatchers.Main) { + progBarCreateDevice.visibility = View.GONE + selectedDevice = device + advance() + } + } catch (e: Throwable) { deviceName.isEnabled = true progBarCreateDevice.visibility = View.GONE - txtvError.text = error.message + txtvError.text = e.message txtvError.visibility = View.VISIBLE - }) + } + } } private fun generateDeviceName(): String { diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/SynchronizationPreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/SynchronizationPreferencesFragment.kt index 5e4a1b65..bf7de1c5 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/SynchronizationPreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/SynchronizationPreferencesFragment.kt @@ -1,5 +1,17 @@ package ac.mdiq.podcini.preferences.fragments.synchronization +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.AlertdialogSyncProviderChooserBinding +import ac.mdiq.podcini.net.sync.SyncService +import ac.mdiq.podcini.net.sync.SynchronizationCredentials +import ac.mdiq.podcini.net.sync.SynchronizationProviderViewData +import ac.mdiq.podcini.net.sync.SynchronizationSettings +import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected +import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey +import ac.mdiq.podcini.ui.activity.PreferenceActivity +import ac.mdiq.podcini.ui.dialog.AuthenticationDialog +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Activity import android.content.DialogInterface import android.os.Bundle @@ -12,24 +24,13 @@ import android.widget.ImageView import android.widget.ListAdapter import android.widget.TextView import androidx.core.text.HtmlCompat +import androidx.lifecycle.lifecycleScope import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.AlertdialogSyncProviderChooserBinding -import ac.mdiq.podcini.ui.activity.PreferenceActivity -import ac.mdiq.podcini.net.sync.SyncService -import ac.mdiq.podcini.net.sync.SynchronizationCredentials -import ac.mdiq.podcini.net.sync.SynchronizationProviderViewData -import ac.mdiq.podcini.net.sync.SynchronizationSettings -import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected -import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey -import ac.mdiq.podcini.ui.dialog.AuthenticationDialog -import ac.mdiq.podcini.util.event.SyncServiceEvent -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch class SynchronizationPreferencesFragment : PreferenceFragmentCompat() { @@ -43,17 +44,27 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() { super.onStart() (activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.synchronization_pref) updateScreen() - EventBus.getDefault().register(this) + procFlowEvents() } override fun onStop() { super.onStop() - EventBus.getDefault().unregister(this) + (activity as PreferenceActivity).supportActionBar!!.subtitle = "" } - @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) - fun syncStatusChanged(event: SyncServiceEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.SyncServiceEvent -> syncStatusChanged(event) + else -> {} + } + } + } + } + + fun syncStatusChanged(event: FlowEvent.SyncServiceEvent) { if (!isProviderConnected && !wifiSyncEnabledKey) return updateScreen() diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/WifiAuthenticationFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/WifiAuthenticationFragment.kt index ed306c6f..dd371a92 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/WifiAuthenticationFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/WifiAuthenticationFragment.kt @@ -7,23 +7,23 @@ import ac.mdiq.podcini.net.sync.SynchronizationSettings.setWifiSyncEnabled import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.hostPort import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.startInstantSync import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.SyncServiceEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Dialog import android.content.Context.WIFI_SERVICE import android.net.wifi.WifiManager import android.os.Bundle -import android.util.Log import android.view.View import android.widget.Button import android.widget.Toast import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import java.util.* @OptIn(UnstableApi::class) class WifiAuthenticationFragment : DialogFragment() { @@ -67,7 +67,7 @@ import java.util.* isGuest = false SynchronizationCredentials.hostport = portNum } - EventBus.getDefault().register(this) + procFlowEvents() return dialog.create() } @@ -93,8 +93,18 @@ import java.util.* } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun syncStatusChanged(event: SyncServiceEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.SyncServiceEvent -> syncStatusChanged(event) + else -> {} + } + } + } + } + + fun syncStatusChanged(event: FlowEvent.SyncServiceEvent) { when (event.messageResId) { R.string.sync_status_error -> { Toast.makeText(requireContext(), event.message, Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt b/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt index 505992f4..5a426067 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt @@ -12,6 +12,7 @@ import ac.mdiq.podcini.storage.model.download.DownloadResult import ac.mdiq.podcini.storage.model.feed.* import ac.mdiq.podcini.storage.model.feed.FeedItemFilter.Companion.unfiltered import ac.mdiq.podcini.storage.model.feed.FeedPreferences.Companion.TAG_ROOT +import ac.mdiq.podcini.util.FeedItemPermutors import ac.mdiq.podcini.util.FeedItemPermutors.getPermutor import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.LongList @@ -19,6 +20,7 @@ import ac.mdiq.podcini.util.comparator.DownloadResultComparator import ac.mdiq.podcini.util.comparator.PlaybackCompletionDateComparator import android.database.Cursor import android.util.Log +import java.util.* import kotlin.math.min @@ -369,7 +371,7 @@ object DBReader { * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order. */ @JvmStatic - fun getPlaybackHistory(offset: Int, limit: Int): List { + fun getPlaybackHistory(offset: Int, limit: Int, start: Long = 0L, end: Long = Date().time, sortOrder: SortOrder = SortOrder.PLAYED_DATE_NEW_OLD): List { Logd(TAG, "getPlaybackHistory() called") val adapter = getInstance() @@ -378,7 +380,7 @@ object DBReader { var mediaCursor: Cursor? = null var itemCursor: Cursor? = null try { - mediaCursor = adapter.getCompletedMediaCursor(offset, limit) + mediaCursor = adapter.getPlayedMediaCursor(offset, limit, start, end) val itemIds = arrayOfNulls(mediaCursor.count) var i = 0 while (i < itemIds.size && mediaCursor.moveToPosition(i)) { @@ -389,7 +391,7 @@ object DBReader { itemCursor = adapter.getFeedItemCursor(itemIds.filterNotNull().toTypedArray()) val items = extractItemlistFromCursor(adapter, itemCursor).toMutableList() loadAdditionalFeedItemListData(items) - items.sortWith(PlaybackCompletionDateComparator()) + getPermutor(sortOrder).reorder(items) return items } finally { mediaCursor?.close() diff --git a/app/src/main/java/ac/mdiq/podcini/storage/DBTasks.kt b/app/src/main/java/ac/mdiq/podcini/storage/DBTasks.kt index 05604ef3..8a2a17ef 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/DBTasks.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/DBTasks.kt @@ -18,15 +18,13 @@ import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.comparator.FeedItemPubdateComparator -import ac.mdiq.podcini.util.event.FeedItemEvent.Companion.updated -import ac.mdiq.podcini.util.event.FeedListUpdateEvent -import ac.mdiq.podcini.util.event.MessageEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.text.TextUtils import android.util.Log import androidx.annotation.VisibleForTesting import androidx.media3.common.util.UnstableApi -import org.greenrobot.eventbus.EventBus import java.util.* import java.util.concurrent.* @@ -90,8 +88,8 @@ import java.util.concurrent.* media.setDownloaded(false) media.setFile_url(null) DBWriter.persistFeedMedia(media) - if (media.item != null) EventBus.getDefault().post(updated(media.item!!)) - EventBus.getDefault().post(MessageEvent(context.getString(R.string.error_file_not_found))) + if (media.item != null) EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(media.item!!)) + EventFlow.postEvent(FlowEvent.MessageEvent(context.getString(R.string.error_file_not_found))) } /** @@ -138,8 +136,8 @@ import java.util.concurrent.* return getFeed(feed.id) } else { val feeds = getFeedList() - for (f in feeds) { - if (f.identifyingValue == feed.identifyingValue) { + for (f in feeds.toList()) { + if (f != null && f.identifyingValue == feed.identifyingValue) { f.items = getFeedItemList(f).toMutableList() return f } @@ -331,8 +329,8 @@ import java.util.concurrent.* adapter.close() - if (savedFeed != null) EventBus.getDefault().post(FeedListUpdateEvent(savedFeed)) - else EventBus.getDefault().post(FeedListUpdateEvent(emptyList())) + if (savedFeed != null) EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(savedFeed)) + else EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(emptyList())) return resultFeed } diff --git a/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt b/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt index a4a66277..9446feef 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/DBWriter.kt @@ -1,6 +1,35 @@ package ac.mdiq.podcini.storage import ac.mdiq.podcini.R +import ac.mdiq.podcini.feed.LocalFeedUpdater.updateFeed +import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface +import ac.mdiq.podcini.net.sync.model.EpisodeAction +import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink +import ac.mdiq.podcini.playback.service.PlaybackServiceConstants +import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.createInstanceFromPreferences +import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId +import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeNoMediaPlaying +import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation +import ac.mdiq.podcini.preferences.UserPreferences.isQueueKeepSorted +import ac.mdiq.podcini.preferences.UserPreferences.queueKeepSortedOrder +import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue +import ac.mdiq.podcini.storage.DBReader.getFeed +import ac.mdiq.podcini.storage.DBReader.getFeedItem +import ac.mdiq.podcini.storage.DBReader.getFeedItemList +import ac.mdiq.podcini.storage.DBReader.getFeedMedia +import ac.mdiq.podcini.storage.DBReader.getQueue +import ac.mdiq.podcini.storage.DBReader.getQueueIDList +import ac.mdiq.podcini.storage.DBTasks.autodownloadUndownloadedItems +import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance +import ac.mdiq.podcini.storage.model.download.DownloadResult +import ac.mdiq.podcini.storage.model.feed.* +import ac.mdiq.podcini.util.FeedItemPermutors.getPermutor +import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast +import ac.mdiq.podcini.util.Logd +import ac.mdiq.podcini.util.LongList +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent +import ac.mdiq.podcini.util.showStackTrace import android.app.backup.BackupManager import android.content.Context import android.net.Uri @@ -9,43 +38,9 @@ import androidx.core.app.NotificationManagerCompat import androidx.documentfile.provider.DocumentFile import androidx.media3.common.util.UnstableApi import com.google.common.util.concurrent.Futures -import ac.mdiq.podcini.feed.FeedEvent -import ac.mdiq.podcini.feed.LocalFeedUpdater.updateFeed -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.createInstanceFromPreferences -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeNoMediaPlaying -import ac.mdiq.podcini.playback.service.PlaybackServiceConstants -import ac.mdiq.podcini.storage.DBReader.getFeed -import ac.mdiq.podcini.storage.DBReader.getFeedItem -import ac.mdiq.podcini.storage.DBReader.getFeedItemList -import ac.mdiq.podcini.storage.DBReader.getFeedMedia -import ac.mdiq.podcini.storage.DBReader.getQueue -import ac.mdiq.podcini.storage.DBReader.getQueueIDList -import ac.mdiq.podcini.storage.DBTasks.autodownloadUndownloadedItems -import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink -import ac.mdiq.podcini.util.FeedItemPermutors.getPermutor -import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast -import ac.mdiq.podcini.util.LongList -import ac.mdiq.podcini.util.event.* -import ac.mdiq.podcini.util.event.FeedItemEvent.Companion.updated -import ac.mdiq.podcini.util.event.QueueEvent.Companion.added -import ac.mdiq.podcini.util.event.QueueEvent.Companion.cleared -import ac.mdiq.podcini.util.event.QueueEvent.Companion.irreversibleRemoved -import ac.mdiq.podcini.util.event.QueueEvent.Companion.moved -import ac.mdiq.podcini.util.event.QueueEvent.Companion.removed -import ac.mdiq.podcini.util.event.playback.PlaybackHistoryEvent -import ac.mdiq.podcini.storage.model.download.DownloadResult -import ac.mdiq.podcini.storage.model.feed.* -import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface -import ac.mdiq.podcini.net.sync.model.EpisodeAction -import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance -import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation -import ac.mdiq.podcini.preferences.UserPreferences.isQueueKeepSorted -import ac.mdiq.podcini.preferences.UserPreferences.queueKeepSortedOrder -import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue -import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.showStackTrace -import org.greenrobot.eventbus.EventBus +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.withContext import java.io.File import java.util.* import java.util.concurrent.ExecutorService @@ -112,7 +107,7 @@ import java.util.concurrent.TimeUnit adapter.close() item.setMedia(null) persistFeedItem(item) - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) } if (result && item != null && shouldDeleteRemoveFromQueue()) removeQueueItemSynchronous(context, false, item.id) } @@ -128,7 +123,7 @@ import java.util.concurrent.TimeUnit // Local feed val documentFile = DocumentFile.fromSingleUri(context, Uri.parse(media.getFile_url())) if (documentFile == null || !documentFile.exists() || !documentFile.delete()) { - EventBus.getDefault().post(MessageEvent(context.getString(R.string.delete_local_failed))) + EventFlow.postEvent(FlowEvent.MessageEvent(context.getString(R.string.delete_local_failed))) return false } media.setFile_url(null) @@ -138,8 +133,8 @@ import java.util.concurrent.TimeUnit // delete downloaded media file val mediaFile = File(url) if (mediaFile.exists() && !mediaFile.delete()) { - val evt = MessageEvent(context.getString(R.string.delete_failed)) - EventBus.getDefault().post(evt) + val evt = FlowEvent.MessageEvent(context.getString(R.string.delete_failed)) + EventFlow.postEvent(evt) return false } media.setDownloaded(false) @@ -171,7 +166,7 @@ import java.util.concurrent.TimeUnit .currentTimestamp() .build() SynchronizationQueueSink.enqueueEpisodeActionIfSynchronizationIsActive(context, action) - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) } } return true @@ -200,7 +195,7 @@ import java.util.concurrent.TimeUnit if (!feed.isLocalFeed && feed.download_url != null) SynchronizationQueueSink.enqueueFeedRemovedIfSynchronizationIsActive(context, feed.download_url!!) - EventBus.getDefault().post(FeedListUpdateEvent(feed)) + EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed)) } } @@ -242,13 +237,13 @@ import java.util.concurrent.TimeUnit adapter.close() for (item in removedFromQueue) { - EventBus.getDefault().post(irreversibleRemoved(item)) + EventFlow.postEvent(FlowEvent.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()) + EventFlow.postEvent(FlowEvent.DownloadLogEvent()) val backupManager = BackupManager(context) backupManager.dataChanged() @@ -263,7 +258,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.clearPlaybackHistory() adapter.close() - EventBus.getDefault().post(PlaybackHistoryEvent.listUpdated()) + EventFlow.postEvent(FlowEvent.HistoryEvent()) } } @@ -276,7 +271,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.clearDownloadLog() adapter.close() - EventBus.getDefault().post(DownloadLogEvent.listUpdated()) + EventFlow.postEvent(FlowEvent.DownloadLogEvent()) } } @@ -303,7 +298,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedMediaPlaybackCompletionDate(media) adapter.close() - EventBus.getDefault().post(PlaybackHistoryEvent.listUpdated()) + EventFlow.postEvent(FlowEvent.HistoryEvent()) } } } @@ -321,7 +316,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setDownloadStatus(status) adapter.close() - EventBus.getDefault().post(DownloadLogEvent.listUpdated()) + EventFlow.postEvent(FlowEvent.DownloadLogEvent()) } } } @@ -350,8 +345,8 @@ import java.util.concurrent.TimeUnit queue.add(index, item) adapter.setQueue(queue) item.addTag(FeedItem.TAG_QUEUE) - EventBus.getDefault().post(added(item, index)) - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.QueueEvent.added(item, index)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) if (item.isNew) markItemPlayed(FeedItem.UNPLAYED, item.id) } } @@ -409,7 +404,7 @@ import java.util.concurrent.TimeUnit var queueModified = false val markAsUnplayedIds = LongList() - val events: MutableList = ArrayList() + val events: MutableList = ArrayList() val updatedItems: MutableList = ArrayList() val positionCalculator = ItemEnqueuePositionCalculator(enqueueLocation) @@ -420,7 +415,7 @@ import java.util.concurrent.TimeUnit val item = getFeedItem(itemId) if (item != null) { queue.add(insertPosition, item) - events.add(added(item, insertPosition)) + events.add(FlowEvent.QueueEvent.added(item, insertPosition)) item.addTag(FeedItem.TAG_QUEUE) updatedItems.add(item) @@ -434,9 +429,9 @@ import java.util.concurrent.TimeUnit applySortOrder(queue, events) adapter.setQueue(queue) for (event in events) { - EventBus.getDefault().post(event) + EventFlow.postEvent(event) } - EventBus.getDefault().post(updated(updatedItems)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(updatedItems)) if (markAsUnplayed && markAsUnplayedIds.size() > 0) markItemPlayed(FeedItem.UNPLAYED, *markAsUnplayedIds.toArray()) } adapter.close() @@ -451,7 +446,7 @@ import java.util.concurrent.TimeUnit * @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 fun applySortOrder(queue: MutableList, events: MutableList) { + private fun applySortOrder(queue: MutableList, events: MutableList) { // queue is not in keep sorted mode, there's nothing to do if (!isQueueKeepSorted) return @@ -466,7 +461,7 @@ import java.util.concurrent.TimeUnit } // Replace ADDED events by a single SORTED event events.clear() - events.add(QueueEvent.sorted(queue)) + events.add(FlowEvent.QueueEvent.sorted(queue)) } /** @@ -479,7 +474,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.clearQueue() adapter.close() - EventBus.getDefault().post(cleared()) + EventFlow.postEvent(FlowEvent.QueueEvent.cleared()) } } @@ -510,7 +505,7 @@ import java.util.concurrent.TimeUnit val queue = getQueue(adapter).toMutableList() var queueModified = false - val events: MutableList = ArrayList() + val events: MutableList = ArrayList() val updatedItems: MutableList = ArrayList() for (itemId in itemIds) { val position = indexInItemList(queue, itemId) @@ -523,7 +518,7 @@ import java.util.concurrent.TimeUnit } queue.removeAt(position) item.removeTag(FeedItem.TAG_QUEUE) - events.add(removed(item)) + events.add(FlowEvent.QueueEvent.removed(item)) updatedItems.add(item) queueModified = true } else { @@ -533,9 +528,9 @@ import java.util.concurrent.TimeUnit if (queueModified) { adapter.setQueue(queue) for (event in events) { - EventBus.getDefault().post(event) + EventFlow.postEvent(event) } - EventBus.getDefault().post(updated(updatedItems)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(updatedItems)) } else Log.w(TAG, "Queue was not modified by call to removeQueueItem") adapter.close() @@ -552,8 +547,8 @@ import java.util.concurrent.TimeUnit adapter.addFavoriteItem(item) adapter.close() item.addTag(FeedItem.TAG_FAVORITE) - EventBus.getDefault().post(FavoritesEvent()) - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FavoritesEvent()) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) } } @@ -563,8 +558,8 @@ import java.util.concurrent.TimeUnit adapter.removeFavoriteItem(item) adapter.close() item.removeTag(FeedItem.TAG_FAVORITE) - EventBus.getDefault().post(FavoritesEvent()) - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FavoritesEvent()) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) } } @@ -634,7 +629,7 @@ import java.util.concurrent.TimeUnit val item: FeedItem = queue.removeAt(from) queue.add(to, item) adapter.setQueue(queue) - if (broadcastUpdate) EventBus.getDefault().post(moved(item, to)) + if (broadcastUpdate) EventFlow.postEvent(FlowEvent.QueueEvent.moved(item, to)) } } else Log.e(TAG, "moveQueueItemHelper: Could not load queue") @@ -678,7 +673,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedItemRead(played, *itemIds) adapter.close() - if (broadcastUpdate) EventBus.getDefault().post(UnreadItemsUpdateEvent()) + if (broadcastUpdate) EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) } } @@ -701,7 +696,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedItemRead(played, itemId, mediaId, resetMediaPosition) adapter.close() - EventBus.getDefault().post(UnreadItemsUpdateEvent()) + EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) } } @@ -716,7 +711,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedItems(FeedItem.NEW, FeedItem.UNPLAYED, feedId) adapter.close() - EventBus.getDefault().post(UnreadItemsUpdateEvent()) + EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) } } @@ -730,7 +725,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedItems(FeedItem.NEW, FeedItem.UNPLAYED) adapter.close() - EventBus.getDefault().post(UnreadItemsUpdateEvent()) + EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) } } @@ -766,7 +761,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.storeFeedItemlist(items) adapter.close() - EventBus.getDefault().post(updated(items)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(items)) } } @@ -819,7 +814,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setSingleFeedItem(item) adapter.close() - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) } } } @@ -848,7 +843,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedPreferences(preferences) adapter.close() - EventBus.getDefault().post(FeedListUpdateEvent(preferences.feedID)) + EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(preferences.feedID)) } } @@ -876,7 +871,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed) adapter.close() - EventBus.getDefault().post(FeedListUpdateEvent(feedId)) + EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feedId)) } } @@ -886,7 +881,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedCustomTitle(feed.id, feed.getCustomTitle()) adapter.close() - EventBus.getDefault().post(FeedListUpdateEvent(feed)) + EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed)) } } @@ -910,7 +905,7 @@ import java.util.concurrent.TimeUnit permutor.reorder(queue) adapter.setQueue(queue) - if (broadcastUpdate) EventBus.getDefault().post(QueueEvent.sorted(queue)) + if (broadcastUpdate) EventFlow.postEvent(FlowEvent.QueueEvent.sorted(queue)) adapter.close() } } @@ -928,7 +923,7 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedItemFilter(feedId, filterValues) adapter.close() - EventBus.getDefault().post(FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId)) + EventFlow.postEvent(FlowEvent.FeedEvent(FlowEvent.FeedEvent.Action.FILTER_CHANGED, feedId)) } } @@ -942,22 +937,31 @@ import java.util.concurrent.TimeUnit adapter.open() adapter.setFeedItemSortOrder(feedId, sortOrder) adapter.close() - EventBus.getDefault().post(FeedEvent(FeedEvent.Action.SORT_ORDER_CHANGED, feedId)) + EventFlow.postEvent(FlowEvent.FeedEvent(FlowEvent.FeedEvent.Action.SORT_ORDER_CHANGED, feedId)) } } /** * Reset the statistics in DB */ - fun resetStatistics(): Future<*> { - return runOnDbThread { +// fun resetStatistics(): Future<*> { +// return runOnDbThread { +// val adapter = getInstance() +// adapter.open() +// adapter.resetAllMediaPlayedDuration() +// adapter.close() +// } +// } + + suspend fun resetStatistics(): Unit = withContext(Dispatchers.IO) { + val result = async { val adapter = getInstance() adapter.open() adapter.resetAllMediaPlayedDuration() adapter.close() } + result.await() } - /** * Submit to the DB thread only if caller is not already on the DB thread. Otherwise, * just execute synchronously diff --git a/app/src/main/java/ac/mdiq/podcini/storage/asynctask/ExportWorker.kt b/app/src/main/java/ac/mdiq/podcini/storage/asynctask/ExportWorker.kt index 966baf85..4e9bac93 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/asynctask/ExportWorker.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/asynctask/ExportWorker.kt @@ -17,6 +17,7 @@ import java.nio.charset.Charset * Writes an OPML file into the export directory in the background. */ class ExportWorker private constructor(private val exportWriter: ExportWriter, private val output: File, private val context: Context) { + constructor(exportWriter: ExportWriter, context: Context) : this(exportWriter, File(getDataFolder(EXPORT_DIR), DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()), context) diff --git a/app/src/main/java/ac/mdiq/podcini/storage/database/PodDBAdapter.kt b/app/src/main/java/ac/mdiq/podcini/storage/database/PodDBAdapter.kt index 80024641..1ca410a6 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/database/PodDBAdapter.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/database/PodDBAdapter.kt @@ -697,6 +697,15 @@ class PodDBAdapter private constructor() { val completedMediaLength: Long get() = DatabaseUtils.queryNumEntries(db, TABLE_NAME_FEED_MEDIA, "$KEY_PLAYBACK_COMPLETION_DATE> 0") + fun getPlayedMediaCursor(offset: Int, limit: Int, start: Long = 0, end: Long = Date().time): Cursor { + require(limit >= 0) { "Limit must be >= 0" } + + return db.query(TABLE_NAME_FEED_MEDIA, null, + String.format(Locale.US, "%s > %d AND %s <= % d", KEY_LAST_PLAYED_TIME, start, KEY_LAST_PLAYED_TIME, end), + null, null, + null, String.format(Locale.US, "%s DESC LIMIT %d, %d", KEY_LAST_PLAYED_TIME, offset, limit)) + } + fun getSingleFeedMediaCursor(id: Long): Cursor { val query = ("SELECT $KEYS_FEED_MEDIA FROM $TABLE_NAME_FEED_MEDIA WHERE $KEY_ID=$id") return db.rawQuery(query, null) diff --git a/app/src/main/java/ac/mdiq/podcini/storage/database/mapper/FeedItemSortQuery.kt b/app/src/main/java/ac/mdiq/podcini/storage/database/mapper/FeedItemSortQuery.kt index 930d3ebe..e1bde757 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/database/mapper/FeedItemSortQuery.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/database/mapper/FeedItemSortQuery.kt @@ -14,6 +14,12 @@ object FeedItemSortQuery { SortOrder.EPISODE_TITLE_Z_A -> PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_TITLE + " " + "DESC" SortOrder.DATE_OLD_NEW -> PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_PUBDATE + " " + "ASC" SortOrder.DATE_NEW_OLD -> PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_PUBDATE + " " + "DESC" + + SortOrder.PLAYED_DATE_OLD_NEW -> PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_LAST_PLAYED_TIME + " " + "ASC" + SortOrder.PLAYED_DATE_NEW_OLD -> PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_LAST_PLAYED_TIME + " " + "DESC" + SortOrder.COMPLETED_DATE_OLD_NEW -> PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE + " " + "ASC" + SortOrder.COMPLETED_DATE_NEW_OLD -> PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE + " " + "DESC" + SortOrder.DURATION_SHORT_LONG -> PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DURATION + " " + "ASC" SortOrder.DURATION_LONG_SHORT -> PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_DURATION + " " + "DESC" SortOrder.SIZE_SMALL_LARGE -> PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + PodDBAdapter.KEY_SIZE + " " + "ASC" diff --git a/app/src/main/java/ac/mdiq/podcini/storage/model/feed/SortOrder.kt b/app/src/main/java/ac/mdiq/podcini/storage/model/feed/SortOrder.kt index 0253d30b..4cc74a33 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/model/feed/SortOrder.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/model/feed/SortOrder.kt @@ -14,9 +14,13 @@ enum class SortOrder(@JvmField val code: Int, @JvmField val scope: Scope) { EPISODE_FILENAME_Z_A(8, Scope.INTRA_FEED), SIZE_SMALL_LARGE(9, Scope.INTRA_FEED), SIZE_LARGE_SMALL(10, Scope.INTRA_FEED), + PLAYED_DATE_OLD_NEW(11, Scope.INTRA_FEED), + PLAYED_DATE_NEW_OLD(12, Scope.INTRA_FEED), + COMPLETED_DATE_OLD_NEW(13, Scope.INTRA_FEED), + COMPLETED_DATE_NEW_OLD(14, Scope.INTRA_FEED), + FEED_TITLE_A_Z(101, Scope.INTER_FEED), FEED_TITLE_Z_A(102, Scope.INTER_FEED), - RANDOM(103, Scope.INTER_FEED), SMART_SHUFFLE_OLD_NEW(104, Scope.INTER_FEED), SMART_SHUFFLE_NEW_OLD(105, Scope.INTER_FEED); diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt index f3b38af4..fb3bc4e6 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt @@ -7,11 +7,11 @@ import ac.mdiq.podcini.storage.DBTasks import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.playback.StartPlayEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.widget.Toast import androidx.media3.common.util.UnstableApi -import org.greenrobot.eventbus.EventBus class PlayActionButton(item: FeedItem) : ItemActionButton(item) { override fun getLabel(): Int { @@ -35,7 +35,7 @@ class PlayActionButton(item: FeedItem) : ItemActionButton(item) { PlaybackServiceStarter(context, media) .callEvenIfRunning(true) .start() - EventBus.getDefault().post(StartPlayEvent(item)) + EventFlow.postEvent(FlowEvent.StartPlayEvent(item)) if (media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt index 6392c015..2f17ff4e 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt @@ -1,20 +1,19 @@ package ac.mdiq.podcini.ui.actions.actionbutton -import android.content.Context -import androidx.media3.common.util.UnstableApi import ac.mdiq.podcini.R +import ac.mdiq.podcini.playback.PlaybackServiceStarter +import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent import ac.mdiq.podcini.preferences.UsageStatistics import ac.mdiq.podcini.preferences.UsageStatistics.logAction -import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed -import ac.mdiq.podcini.playback.PlaybackServiceStarter -import ac.mdiq.podcini.ui.dialog.StreamingConfirmationDialog import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.RemoteMedia -import ac.mdiq.podcini.util.event.playback.StartPlayEvent -import android.util.Log -import org.greenrobot.eventbus.EventBus +import ac.mdiq.podcini.ui.dialog.StreamingConfirmationDialog +import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent +import android.content.Context +import androidx.media3.common.util.UnstableApi class StreamActionButton(item: FeedItem) : ItemActionButton(item) { override fun getLabel(): Int { @@ -40,7 +39,7 @@ class StreamActionButton(item: FeedItem) : ItemActionButton(item) { .shouldStreamThisTime(true) .callEvenIfRunning(true) .start() - EventBus.getDefault().post(StartPlayEvent(item)) + EventFlow.postEvent(FlowEvent.StartPlayEvent(item)) if (media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt index 86f0132d..5dda7791 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/TTSActionButton.kt @@ -3,7 +3,6 @@ package ac.mdiq.podcini.ui.actions.actionbutton import ac.mdiq.podcini.R import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.getMediafilePath import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.getMediafilename -import ac.mdiq.podcini.util.AudioMediaOperation.mergeAudios import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.storage.DBWriter.persistFeedItem import ac.mdiq.podcini.storage.model.feed.FeedItem @@ -11,9 +10,11 @@ import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.ui.fragment.FeedItemlistFragment.Companion.tts import ac.mdiq.podcini.ui.fragment.FeedItemlistFragment.Companion.ttsReady import ac.mdiq.podcini.ui.fragment.FeedItemlistFragment.Companion.ttsWorking +import ac.mdiq.podcini.util.AudioMediaOperation.mergeAudios import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.NetworkUtils.fetchHtmlSource -import ac.mdiq.podcini.util.event.FeedItemEvent.Companion.updated +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech.getMaxSpeechInputLength @@ -26,7 +27,6 @@ import androidx.core.text.HtmlCompat import androidx.media3.common.util.UnstableApi import kotlinx.coroutines.* import net.dankito.readability4j.Readability4J -import org.greenrobot.eventbus.EventBus import java.io.File import java.util.* import kotlin.math.max @@ -52,7 +52,7 @@ class TTSActionButton(item: FeedItem) : ItemActionButton(item) { } processing = 0.01f item.setBuilding() - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) ioScope.launch { if (item.transcript == null) { runBlocking { @@ -66,12 +66,12 @@ class TTSActionButton(item: FeedItem) : ItemActionButton(item) { } } else readerText = HtmlCompat.fromHtml(item.transcript!!, HtmlCompat.FROM_HTML_MODE_COMPACT).toString() processing = 0.1f - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) if (!readerText.isNullOrEmpty()) { while (!ttsReady) Thread.sleep(100) processing = 0.15f - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) while (ttsWorking) Thread.sleep(100) ttsWorking = true if (item.feed?.language != null) { @@ -123,10 +123,10 @@ class TTSActionButton(item: FeedItem) : ItemActionButton(item) { i++ while (i-j > 0) Thread.sleep(100) processing = 0.15f + 0.7f * startIndex / readerText!!.length - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) } processing = 0.85f - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) if (status == TextToSpeech.SUCCESS) { mergeAudios(parts.toTypedArray(), mediaFile.absolutePath, null) @@ -157,7 +157,7 @@ class TTSActionButton(item: FeedItem) : ItemActionButton(item) { item.setPlayed(false) processing = 1f - EventBus.getDefault().post(updated(item)) + EventFlow.postEvent(FlowEvent.FeedItemEvent.updated(item)) } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt index 4f6deb8b..bf118dae 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/swipeactions/SwipeActions.kt @@ -1,5 +1,13 @@ package ac.mdiq.podcini.ui.actions.swipeactions +import ac.mdiq.podcini.R +import ac.mdiq.podcini.storage.model.feed.FeedItemFilter +import ac.mdiq.podcini.ui.dialog.SwipeActionsDialog +import ac.mdiq.podcini.ui.fragment.* +import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr +import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.graphics.Canvas import androidx.core.graphics.ColorUtils @@ -11,15 +19,7 @@ import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView import com.annimon.stream.Stream -import ac.mdiq.podcini.R -import ac.mdiq.podcini.ui.dialog.SwipeActionsDialog -import ac.mdiq.podcini.ui.fragment.* -import ac.mdiq.podcini.storage.model.feed.FeedItemFilter -import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr -import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder -import ac.mdiq.podcini.util.event.SwipeActionsChangedEvent import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator -import org.greenrobot.eventbus.EventBus import java.util.* import kotlin.math.max import kotlin.math.min @@ -90,7 +90,7 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v SwipeActionsDialog(fragment.requireContext(), tag).show(object : SwipeActionsDialog.Callback { override fun onCall() { this@SwipeActions.reloadPreference() - EventBus.getDefault().post(SwipeActionsChangedEvent()) + EventFlow.postEvent(FlowEvent.SwipeActionsChangedEvent()) } }) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt index e94aba42..a8ca7cfb 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -25,9 +25,8 @@ import ac.mdiq.podcini.ui.statistics.StatisticsFragment import ac.mdiq.podcini.ui.utils.ThemeUtils.getDrawableFromAttr import ac.mdiq.podcini.ui.view.LockableBottomSheetBehavior import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.EpisodeDownloadEvent -import ac.mdiq.podcini.util.event.FeedUpdateRunningEvent -import ac.mdiq.podcini.util.event.MessageEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.Manifest import android.annotation.SuppressLint import android.content.ComponentName @@ -57,6 +56,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentContainerView +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaController import androidx.media3.session.SessionToken @@ -69,10 +69,9 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCa import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.common.util.concurrent.MoreExecutors +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.apache.commons.lang3.ArrayUtils -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import kotlin.math.min /** @@ -196,7 +195,7 @@ class MainActivity : CastEnabledActivity() { } } } - EventBus.getDefault().postSticky(FeedUpdateRunningEvent(isRefreshingFeeds)) + EventFlow.postStickyEvent(FlowEvent.FeedUpdateRunningEvent(isRefreshingFeeds)) } WorkManager.getInstance(this) .getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG) @@ -232,7 +231,7 @@ class MainActivity : CastEnabledActivity() { updatedEpisodes[downloadUrl] = DownloadStatus(status, progress) } DownloadServiceInterface.get()?.setCurrentDownloads(updatedEpisodes) - EventBus.getDefault().postSticky(EpisodeDownloadEvent(updatedEpisodes)) + EventFlow.postStickyEvent(FlowEvent.EpisodeDownloadEvent(updatedEpisodes)) } } @@ -483,7 +482,7 @@ class MainActivity : CastEnabledActivity() { public override fun onStart() { super.onStart() - EventBus.getDefault().register(this) + procFlowEvents() RatingDialog.init(this) val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java)) @@ -517,7 +516,7 @@ class MainActivity : CastEnabledActivity() { override fun onStop() { super.onStop() - EventBus.getDefault().unregister(this) + } override fun onTrimMemory(level: Int) { @@ -558,10 +557,19 @@ class MainActivity : CastEnabledActivity() { } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: MessageEvent) { - Logd(TAG, "onEvent($event)") + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.MessageEvent -> onEventMainThread(event) + else -> {} + } + } + } + } + fun onEventMainThread(event: FlowEvent.MessageEvent) { + Logd(TAG, "onEvent($event)") val snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_LONG) if (event.action != null) snackbar.setAction(event.actionText) { event.action.accept(this) } } @@ -674,7 +682,7 @@ class MainActivity : CastEnabledActivity() { val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager var customKeyCode: Int? = null - EventBus.getDefault().post(event) + EventFlow.postEvent(event) when (keyCode) { KeyEvent.KEYCODE_P -> customKeyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt index 56077731..dd6d5bef 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt @@ -30,12 +30,12 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import com.google.android.material.dialog.MaterialAlertDialogBuilder -import io.reactivex.Completable -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.apache.commons.io.input.BOMInputStream import java.io.InputStreamReader import java.io.Reader @@ -62,57 +62,84 @@ class OpmlImportActivity : AppCompatActivity() { setContentView(binding.root) binding.feedlist.choiceMode = ListView.CHOICE_MODE_MULTIPLE - binding.feedlist.onItemClickListener = - OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long -> - val checked = binding.feedlist.checkedItemPositions - var checkedCount = 0 - for (i in 0 until checked.size()) { - if (checked.valueAt(i)) checkedCount++ - } - if (listAdapter != null) { - if (checkedCount == listAdapter!!.count) { - selectAll.setVisible(false) - deselectAll.setVisible(true) - } else { - deselectAll.setVisible(false) - selectAll.setVisible(true) - } + binding.feedlist.onItemClickListener = OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long -> + val checked = binding.feedlist.checkedItemPositions + var checkedCount = 0 + for (i in 0 until checked.size()) { + if (checked.valueAt(i)) checkedCount++ + } + if (listAdapter != null) { + if (checkedCount == listAdapter!!.count) { + selectAll.setVisible(false) + deselectAll.setVisible(true) + } else { + deselectAll.setVisible(false) + selectAll.setVisible(true) } } + } binding.butCancel.setOnClickListener { setResult(RESULT_CANCELED) finish() } binding.butConfirm.setOnClickListener { binding.progressBar.visibility = View.VISIBLE - Completable.fromAction { - val checked = binding.feedlist.checkedItemPositions - for (i in 0 until checked.size()) { - if (!checked.valueAt(i)) continue +// Completable.fromAction { +// val checked = binding.feedlist.checkedItemPositions +// for (i in 0 until checked.size()) { +// if (!checked.valueAt(i)) continue +// +// if (!readElements.isNullOrEmpty()) { +// val element = readElements!![checked.keyAt(i)] +// val feed = Feed(element.xmlUrl, null, if (element.text != null) element.text else "Unknown podcast") +// feed.items = mutableListOf() +// DBTasks.updateFeed(this, feed, false) +// } +// } +// runOnce(this) +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe( +// { +// binding.progressBar.visibility = View.GONE +// val intent = Intent(this@OpmlImportActivity, MainActivity::class.java) +// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) +// startActivity(intent) +// finish() +// }, { e: Throwable -> +// e.printStackTrace() +// binding.progressBar.visibility = View.GONE +// Toast.makeText(this, e.message, Toast.LENGTH_LONG).show() +// }) - if (!readElements.isNullOrEmpty()) { - val element = readElements!![checked.keyAt(i)] - val feed = Feed(element.xmlUrl, null, if (element.text != null) element.text else "Unknown podcast") - feed.items = mutableListOf() - DBTasks.updateFeed(this, feed, false) + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + val checked = binding.feedlist.checkedItemPositions + for (i in 0 until checked.size()) { + if (!checked.valueAt(i)) continue + + if (!readElements.isNullOrEmpty()) { + val element = readElements!![checked.keyAt(i)] + val feed = Feed(element.xmlUrl, null, if (element.text != null) element.text else "Unknown podcast") + feed.items = mutableListOf() + DBTasks.updateFeed(this@OpmlImportActivity, feed, false) + } + } + runOnce(this@OpmlImportActivity) } + binding.progressBar.visibility = View.GONE + val intent = Intent(this@OpmlImportActivity, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + finish() + } catch (e: Throwable) { + e.printStackTrace() + binding.progressBar.visibility = View.GONE + Toast.makeText(this@OpmlImportActivity, (e.message ?: "Import error"), Toast.LENGTH_LONG).show() } - runOnce(this) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { - binding.progressBar.visibility = View.GONE - val intent = Intent(this@OpmlImportActivity, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - finish() - }, { e: Throwable -> - e.printStackTrace() - binding.progressBar.visibility = View.GONE - Toast.makeText(this, e.message, Toast.LENGTH_LONG).show() - }) } var uri = intent.data @@ -203,38 +230,83 @@ class OpmlImportActivity : AppCompatActivity() { private fun startImport() { binding.progressBar.visibility = View.VISIBLE - Observable.fromCallable { - val opmlFileStream = contentResolver.openInputStream(uri!!) - val bomInputStream = BOMInputStream(opmlFileStream) - val bom = bomInputStream.bom - val charsetName = if (bom == null) "UTF-8" else bom.charsetName - val reader: Reader = InputStreamReader(bomInputStream, charsetName) - val opmlReader = OpmlReader() - val result = opmlReader.readDocument(reader) - reader.close() - result - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { result: ArrayList? -> +// Observable.fromCallable { +// val opmlFileStream = contentResolver.openInputStream(uri!!) +// val bomInputStream = BOMInputStream(opmlFileStream) +// val bom = bomInputStream.bom +// val charsetName = if (bom == null) "UTF-8" else bom.charsetName +// val reader: Reader = InputStreamReader(bomInputStream, charsetName) +// val opmlReader = OpmlReader() +// val result = opmlReader.readDocument(reader) +// reader.close() +// result +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe( +// { result: ArrayList? -> +// binding.progressBar.visibility = View.GONE +// Logd(TAG, "Parsing was successful") +// readElements = result +// listAdapter = ArrayAdapter(this@OpmlImportActivity, android.R.layout.simple_list_item_multiple_choice, titleList) +// binding.feedlist.adapter = listAdapter +// }, { e: Throwable -> +// Logd(TAG, Log.getStackTraceString(e)) +// val message = if (e.message == null) "" else e.message!! +// if (message.lowercase().contains("permission")) { +// val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) +// if (permission != PackageManager.PERMISSION_GRANTED) { +// requestPermission() +// return@subscribe +// } +// } +// binding.progressBar.visibility = View.GONE +// val alert = MaterialAlertDialogBuilder(this) +// alert.setTitle(R.string.error_label) +// val userReadable = getString(R.string.opml_reader_error) +// val details = e.message +// val total = """ +// $userReadable +// +// $details +// """.trimIndent() +// val errorMessage = SpannableString(total) +// errorMessage.setSpan(ForegroundColorSpan(-0x77777778), userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) +// alert.setMessage(errorMessage) +// alert.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> finish() } +// alert.show() +// }) + + lifecycleScope.launch(Dispatchers.IO) { + try { + val opmlFileStream = contentResolver.openInputStream(uri!!) + val bomInputStream = BOMInputStream(opmlFileStream) + val bom = bomInputStream.bom + val charsetName = if (bom == null) "UTF-8" else bom.charsetName + val reader: Reader = InputStreamReader(bomInputStream, charsetName) + val opmlReader = OpmlReader() + val result = opmlReader.readDocument(reader) + reader.close() + withContext(Dispatchers.Main) { binding.progressBar.visibility = View.GONE Logd(TAG, "Parsing was successful") readElements = result listAdapter = ArrayAdapter(this@OpmlImportActivity, android.R.layout.simple_list_item_multiple_choice, titleList) binding.feedlist.adapter = listAdapter - }, { e: Throwable -> + } + } catch (e: Throwable) { + withContext(Dispatchers.Main) { Logd(TAG, Log.getStackTraceString(e)) val message = if (e.message == null) "" else e.message!! if (message.lowercase().contains("permission")) { - val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + val permission = ActivityCompat.checkSelfPermission(this@OpmlImportActivity, Manifest.permission.READ_EXTERNAL_STORAGE) if (permission != PackageManager.PERMISSION_GRANTED) { requestPermission() - return@subscribe + return@withContext } } binding.progressBar.visibility = View.GONE - val alert = MaterialAlertDialogBuilder(this) + val alert = MaterialAlertDialogBuilder(this@OpmlImportActivity) alert.setTitle(R.string.error_label) val userReadable = getString(R.string.opml_reader_error) val details = e.message @@ -248,7 +320,9 @@ class OpmlImportActivity : AppCompatActivity() { alert.setMessage(errorMessage) alert.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> finish() } alert.show() - }) + } + } + } } override fun onDestroy() { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt index 2755a897..3d7838e5 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt @@ -6,7 +6,8 @@ import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme import ac.mdiq.podcini.preferences.fragments.* import ac.mdiq.podcini.preferences.fragments.synchronization.SynchronizationPreferencesFragment import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.MessageEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.annotation.SuppressLint import android.content.Intent import android.os.Build @@ -16,14 +17,14 @@ import android.view.MenuItem import android.view.View import android.view.inputmethod.InputMethodManager import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceFragmentCompat import com.bytehamster.lib.preferencesearch.SearchPreferenceResult import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch /** * PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see @@ -144,12 +145,12 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { override fun onStart() { super.onStart() - EventBus.getDefault().register(this) + procFlowEvents() } override fun onStop() { super.onStop() - EventBus.getDefault().unregister(this) + } override fun onDestroy() { @@ -157,8 +158,18 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { _binding = null } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: MessageEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.MessageEvent -> onEventMainThread(event) + else -> {} + } + } + } + } + + fun onEventMainThread(event: FlowEvent.MessageEvent) { Logd(FRAGMENT_TAG, "onEvent($event)") val s = Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG) if (event.action != null) { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt index e4dad2d1..8015be75 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt @@ -23,6 +23,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import coil.imageLoader import coil.request.ErrorResult @@ -41,7 +42,7 @@ class SelectSubscriptionActivity : AppCompatActivity() { @Volatile private var listItems: List = listOf() - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) // private var disposable: Disposable? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -138,7 +139,7 @@ class SelectSubscriptionActivity : AppCompatActivity() { // binding.list.adapter = adapter // }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - scope.launch { + lifecycleScope.launch { try { val result = withContext(Dispatchers.IO) { val data: NavDrawerData = DBReader.getNavDrawerData(UserPreferences.subscriptionsFilter) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/SplashActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/SplashActivity.kt index 5aa7cf07..fc333a12 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/SplashActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/SplashActivity.kt @@ -1,5 +1,7 @@ package ac.mdiq.podcini.ui.activity +import ac.mdiq.podcini.storage.database.PodDBAdapter +import ac.mdiq.podcini.util.error.CrashReportWriter import android.annotation.SuppressLint import android.app.Activity import android.content.Intent @@ -7,12 +9,10 @@ import android.os.Bundle import android.view.View import android.widget.Toast import androidx.media3.common.util.UnstableApi -import ac.mdiq.podcini.util.error.CrashReportWriter -import ac.mdiq.podcini.storage.database.PodDBAdapter -import io.reactivex.Completable -import io.reactivex.CompletableEmitter -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Shows the Podcini logo while waiting for the main activity to start. @@ -24,24 +24,44 @@ class SplashActivity : Activity() { val content = findViewById(android.R.id.content) content.viewTreeObserver.addOnPreDrawListener { false } // Keep splash screen active - Completable.create { subscriber: CompletableEmitter -> - // Trigger schema updates - PodDBAdapter.getInstance().open() - PodDBAdapter.getInstance().close() - subscriber.onComplete() +// Completable.create { subscriber: CompletableEmitter -> +// // Trigger schema updates +// PodDBAdapter.getInstance().open() +// PodDBAdapter.getInstance().close() +// subscriber.onComplete() +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ +// val intent = Intent(this@SplashActivity, MainActivity::class.java) +// startActivity(intent) +// overridePendingTransition(0, 0) +// finish() +// }, { error: Throwable -> +// error.printStackTrace() +// CrashReportWriter.write(error) +// Toast.makeText(this, error.localizedMessage, Toast.LENGTH_LONG).show() +// finish() +// }) + + val scope = CoroutineScope(Dispatchers.IO) + scope.launch(Dispatchers.IO) { + try { + PodDBAdapter.getInstance().open() + PodDBAdapter.getInstance().close() + withContext(Dispatchers.Main) { + val intent = Intent(this@SplashActivity, MainActivity::class.java) + startActivity(intent) + overridePendingTransition(0, 0) + finish() + } + } catch (e: Throwable) { + e.printStackTrace() + CrashReportWriter.write(e) + Toast.makeText(this@SplashActivity, e.localizedMessage, Toast.LENGTH_LONG).show() + finish() + } } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ - val intent = Intent(this@SplashActivity, MainActivity::class.java) - startActivity(intent) - overridePendingTransition(0, 0) - finish() - }, { error: Throwable -> - error.printStackTrace() - CrashReportWriter.write(error) - Toast.makeText(this, error.localizedMessage, Toast.LENGTH_LONG).show() - finish() - }) + } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index 406ebfd3..bb9c1b40 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -20,10 +20,8 @@ import ac.mdiq.podcini.util.FeedItemUtil.getLinkWithFallback import ac.mdiq.podcini.util.IntentUtils.openInBrowser import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare -import ac.mdiq.podcini.util.event.MessageEvent -import ac.mdiq.podcini.util.event.PlayerErrorEvent -import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent -import ac.mdiq.podcini.util.event.playback.SleepTimerUpdatedEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.DialogInterface import android.content.Intent import android.content.pm.ActivityInfo @@ -36,11 +34,11 @@ import android.util.Log import android.view.* import android.view.MenuItem.SHOW_AS_ACTION_NEVER import android.widget.EditText +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch /** * Activity for playing video files. @@ -137,7 +135,7 @@ class VideoplayerActivity : CastEnabledActivity() { @UnstableApi override fun onStop() { - EventBus.getDefault().unregister(this) + super.onStop() } @@ -148,7 +146,7 @@ class VideoplayerActivity : CastEnabledActivity() { @UnstableApi override fun onStart() { super.onStart() - EventBus.getDefault().register(this) + procFlowEvents() } override fun onTrimMemory(level: Int) { @@ -168,24 +166,21 @@ class VideoplayerActivity : CastEnabledActivity() { startActivity(newIntent) } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) { - if (event.isCancelled || event.wasJustEnabled()) supportInvalidateOptionsMenu() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) supportInvalidateOptionsMenu() + is FlowEvent.PlaybackServiceEvent -> if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) finish() + is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(this@VideoplayerActivity, event) + is FlowEvent.MessageEvent -> onEventMainThread(event) + else -> {} + } + } + } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) finish() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onMediaPlayerError(event: PlayerErrorEvent) { - MediaPlayerErrorDialog.show(this, event) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: MessageEvent) { + fun onEventMainThread(event: FlowEvent.MessageEvent) { Logd(TAG, "onEvent($event)") val errorDialog = MaterialAlertDialogBuilder(this) errorDialog.setMessage(event.message) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/adapter/EpisodeItemListAdapter.kt b/app/src/main/java/ac/mdiq/podcini/ui/adapter/EpisodeItemListAdapter.kt index e0e9f344..b50bde0c 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/adapter/EpisodeItemListAdapter.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/adapter/EpisodeItemListAdapter.kt @@ -21,7 +21,8 @@ import java.lang.ref.WeakReference open class EpisodeItemListAdapter(mainActivity: MainActivity) : SelectableAdapter(mainActivity), View.OnCreateContextMenuListener { - private val mainActivityRef: WeakReference = WeakReference(mainActivity) + val mainActivityRef: WeakReference = WeakReference(mainActivity) + private var episodes: List = ArrayList() var longPressedItem: FeedItem? = null private var longPressedPosition: Int = 0 // used to init actionMode diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/AllEpisodesFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/AllEpisodesFilterDialog.kt index 3e2b48f6..db87537b 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/AllEpisodesFilterDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/AllEpisodesFilterDialog.kt @@ -1,15 +1,15 @@ package ac.mdiq.podcini.ui.dialog -import android.os.Bundle import ac.mdiq.podcini.storage.model.feed.FeedItemFilter -import org.greenrobot.eventbus.EventBus +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent +import android.os.Bundle class AllEpisodesFilterDialog : ItemFilterDialog() { override fun onFilterChanged(newFilterValues: Set) { - EventBus.getDefault().post(AllEpisodesFilterChangedEvent(newFilterValues)) + EventFlow.postEvent(FlowEvent.AllEpisodesFilterChangedEvent(newFilterValues)) } - class AllEpisodesFilterChangedEvent(val filterValues: Set?) companion object { fun newInstance(filter: FeedItemFilter?): AllEpisodesFilterDialog { val dialog = AllEpisodesFilterDialog() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/DownloadLogDetailsDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/DownloadLogDetailsDialog.kt index fc329004..ffa4fd13 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/DownloadLogDetailsDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/DownloadLogDetailsDialog.kt @@ -1,5 +1,13 @@ package ac.mdiq.podcini.ui.dialog +import ac.mdiq.podcini.R +import ac.mdiq.podcini.storage.DBReader +import ac.mdiq.podcini.storage.model.download.DownloadResult +import ac.mdiq.podcini.storage.model.feed.Feed +import ac.mdiq.podcini.storage.model.feed.FeedMedia +import ac.mdiq.podcini.util.DownloadErrorLabel.from +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -8,14 +16,6 @@ import android.view.View import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.storage.DBReader -import ac.mdiq.podcini.util.DownloadErrorLabel.from -import ac.mdiq.podcini.util.event.MessageEvent -import ac.mdiq.podcini.storage.model.download.DownloadResult -import ac.mdiq.podcini.storage.model.feed.Feed -import ac.mdiq.podcini.storage.model.feed.FeedMedia -import org.greenrobot.eventbus.EventBus class DownloadLogDetailsDialog(context: Context, status: DownloadResult) : MaterialAlertDialogBuilder(context) { init { @@ -43,7 +43,7 @@ class DownloadLogDetailsDialog(context: Context, status: DownloadResult) : Mater val clipboard = getContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clip = ClipData.newPlainText(context.getString(R.string.download_error_details), messageFull) clipboard.setPrimaryClip(clip) - if (Build.VERSION.SDK_INT < 32) EventBus.getDefault().post(MessageEvent(context.getString(R.string.copied_to_clipboard))) + if (Build.VERSION.SDK_INT < 32) EventFlow.postEvent(FlowEvent.MessageEvent(context.getString(R.string.copied_to_clipboard))) } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt index 6979123d..134e9e1d 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt @@ -1,13 +1,13 @@ package ac.mdiq.podcini.ui.dialog +import ac.mdiq.podcini.R +import ac.mdiq.podcini.preferences.UserPreferences.feedOrder +import ac.mdiq.podcini.preferences.UserPreferences.setFeedOrder +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.content.DialogInterface import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent -import ac.mdiq.podcini.preferences.UserPreferences.feedOrder -import ac.mdiq.podcini.preferences.UserPreferences.setFeedOrder -import org.greenrobot.eventbus.EventBus object FeedSortDialog { fun showDialog(context: Context) { @@ -24,7 +24,7 @@ object FeedSortDialog { if (selectedIndex != which) { setFeedOrder(entryValues[which]) //Update subscriptions - EventBus.getDefault().post(UnreadItemsUpdateEvent()) + EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) } d.dismiss() } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/ItemSortDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/ItemSortDialog.kt index 1bb7c828..10cffcfc 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/ItemSortDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/ItemSortDialog.kt @@ -1,25 +1,22 @@ package ac.mdiq.podcini.ui.dialog +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.SortDialogBinding +import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding +import ac.mdiq.podcini.databinding.SortDialogItemBinding +import ac.mdiq.podcini.storage.model.feed.SortOrder import android.app.Dialog import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.WindowManager import android.widget.CompoundButton import android.widget.FrameLayout import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.SortDialogBinding -import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding -import ac.mdiq.podcini.databinding.SortDialogItemBinding -import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.storage.model.feed.SortOrder -import android.graphics.Color -import android.util.Log -import android.view.WindowManager open class ItemSortDialog : BottomSheetDialogFragment() { protected var _binding: SortDialogBinding? = null @@ -45,6 +42,8 @@ open class ItemSortDialog : BottomSheetDialogFragment() { onAddItem(R.string.feed_title, SortOrder.FEED_TITLE_A_Z, SortOrder.FEED_TITLE_Z_A, true) onAddItem(R.string.duration, SortOrder.DURATION_SHORT_LONG, SortOrder.DURATION_LONG_SHORT, true) onAddItem(R.string.date, SortOrder.DATE_OLD_NEW, SortOrder.DATE_NEW_OLD, false) + onAddItem(R.string.last_played_date, SortOrder.PLAYED_DATE_OLD_NEW, SortOrder.PLAYED_DATE_NEW_OLD, false) + onAddItem(R.string.completed_date, SortOrder.COMPLETED_DATE_OLD_NEW, SortOrder.COMPLETED_DATE_NEW_OLD, false) onAddItem(R.string.size, SortOrder.SIZE_SMALL_LARGE, SortOrder.SIZE_LARGE_SMALL, false) onAddItem(R.string.filename, SortOrder.EPISODE_FILENAME_A_Z, SortOrder.EPISODE_FILENAME_Z_A, true) onAddItem(R.string.random, SortOrder.RANDOM, SortOrder.RANDOM, true) @@ -87,8 +86,7 @@ open class ItemSortDialog : BottomSheetDialogFragment() { } } - protected open fun onSelectionChanged() { - } + protected open fun onSelectionChanged() {} override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val dialog = super.onCreateDialog(savedInstanceState) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/MediaPlayerErrorDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/MediaPlayerErrorDialog.kt index 54ee0d9b..32f11861 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/MediaPlayerErrorDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/MediaPlayerErrorDialog.kt @@ -9,13 +9,13 @@ import android.text.style.ForegroundColorSpan import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.dialog.MaterialAlertDialogBuilder import ac.mdiq.podcini.R -import ac.mdiq.podcini.util.event.PlayerErrorEvent +import ac.mdiq.podcini.util.event.FlowEvent import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi @OptIn(UnstableApi::class) object MediaPlayerErrorDialog { - fun show(activity: Activity, event: PlayerErrorEvent) { + fun show(activity: Activity, event: FlowEvent.PlayerErrorEvent) { val errorDialog = MaterialAlertDialogBuilder(activity) errorDialog.setTitle(R.string.error_label) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/ProxyDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/ProxyDialog.kt index 4098e1da..fb0c757c 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/ProxyDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/ProxyDialog.kt @@ -1,5 +1,13 @@ package ac.mdiq.podcini.ui.dialog +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.ProxySettingsBinding +import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder +import ac.mdiq.podcini.net.download.service.PodciniHttpClient.reinit +import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setProxyConfig +import ac.mdiq.podcini.preferences.UserPreferences.proxyConfig +import ac.mdiq.podcini.storage.model.download.ProxyConfig +import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr import android.app.Dialog import android.content.Context import android.os.Build @@ -10,23 +18,17 @@ import android.view.View import android.widget.* import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.ProxySettingsBinding -import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder -import ac.mdiq.podcini.net.download.service.PodciniHttpClient.reinit -import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setProxyConfig -import ac.mdiq.podcini.storage.model.download.ProxyConfig -import ac.mdiq.podcini.preferences.UserPreferences.proxyConfig -import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr -import io.reactivex.Completable -import io.reactivex.CompletableEmitter -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import okhttp3.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.Credentials.basic import okhttp3.OkHttpClient +import okhttp3.Request import okhttp3.Request.Builder +import okhttp3.Response +import okhttp3.Route import java.io.IOException import java.net.InetSocketAddress import java.net.Proxy @@ -43,7 +45,7 @@ class ProxyDialog(private val context: Context) { private lateinit var txtvMessage: TextView private var testSuccessful = false - private var disposable: Disposable? = null +// private var disposable: Disposable? = null fun show(): Dialog { val content = View.inflate(context, R.layout.proxy_settings, null) @@ -215,7 +217,7 @@ class ProxyDialog(private val context: Context) { } private fun test() { - disposable?.dispose() +// disposable?.dispose() if (!checkValidity()) { setTestRequired(true) return @@ -227,58 +229,108 @@ class ProxyDialog(private val context: Context) { txtvMessage.setTextColor(textColorPrimary) txtvMessage.text = "{fa-circle-o-notch spin} $checking" txtvMessage.visibility = View.VISIBLE - disposable = Completable.create { emitter: CompletableEmitter -> - val type = spType.selectedItem as String - val host = etHost.text.toString() - val port = etPort.text.toString() - val username = etUsername.text.toString() - val password = etPassword.text.toString() - var portValue = 8080 - if (port.isNotEmpty()) portValue = port.toInt() - val address: SocketAddress = InetSocketAddress.createUnresolved(host, portValue) - val proxyType = Proxy.Type.valueOf(type.uppercase()) - val builder: OkHttpClient.Builder = newBuilder() - .connectTimeout(10, TimeUnit.SECONDS) - .proxy(Proxy(proxyType, address)) - if (username.isNotEmpty()) { - builder.proxyAuthenticator { _: Route?, response: Response -> - val credentials = basic(username, password) - response.request.newBuilder() - .header("Proxy-Authorization", credentials) - .build() - } - } - val client: OkHttpClient = builder.build() - val request: Request = Builder().url("https://www.example.com").head().build() +// disposable = Completable.create { emitter: CompletableEmitter -> +// val type = spType.selectedItem as String +// val host = etHost.text.toString() +// val port = etPort.text.toString() +// val username = etUsername.text.toString() +// val password = etPassword.text.toString() +// var portValue = 8080 +// if (port.isNotEmpty()) portValue = port.toInt() +// +// val address: SocketAddress = InetSocketAddress.createUnresolved(host, portValue) +// val proxyType = Proxy.Type.valueOf(type.uppercase()) +// val builder: OkHttpClient.Builder = newBuilder() +// .connectTimeout(10, TimeUnit.SECONDS) +// .proxy(Proxy(proxyType, address)) +// if (username.isNotEmpty()) { +// builder.proxyAuthenticator { _: Route?, response: Response -> +// val credentials = basic(username, password) +// response.request.newBuilder() +// .header("Proxy-Authorization", credentials) +// .build() +// } +// } +// val client: OkHttpClient = builder.build() +// val request: Request = Builder().url("https://www.example.com").head().build() +// try { +// client.newCall(request).execute().use { response -> +// if (response.isSuccessful) { +// emitter.onComplete() +// } else { +// emitter.onError(IOException(response.message)) +// } +// } +// } catch (e: IOException) { +// emitter.onError(e) +// } +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe( +// { +// txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_green)) +// val message = String.format("%s %s", "{fa-check}", context.getString(R.string.proxy_test_successful)) +// txtvMessage.text = message +// setTestRequired(false) +// }, +// { error: Throwable -> +// error.printStackTrace() +// txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_red)) +// val message = String.format("%s %s: %s", "{fa-close}", context.getString(R.string.proxy_test_failed), error.message) +// txtvMessage.text = message +// setTestRequired(true) +// } +// ) + + val coroutineScope = CoroutineScope(Dispatchers.Main) + coroutineScope.launch(Dispatchers.IO) { try { - client.newCall(request).execute().use { response -> - if (response.isSuccessful) { - emitter.onComplete() - } else { - emitter.onError(IOException(response.message)) + val type = spType.selectedItem as String + val host = etHost.text.toString() + val port = etPort.text.toString() + val username = etUsername.text.toString() + val password = etPassword.text.toString() + var portValue = 8080 + if (port.isNotEmpty()) portValue = port.toInt() + + val address: SocketAddress = InetSocketAddress.createUnresolved(host, portValue) + val proxyType = Proxy.Type.valueOf(type.uppercase()) + val builder: OkHttpClient.Builder = newBuilder() + .connectTimeout(10, TimeUnit.SECONDS) + .proxy(Proxy(proxyType, address)) + if (username.isNotEmpty()) { + builder.proxyAuthenticator { _: Route?, response: Response -> + val credentials = basic(username, password) + response.request.newBuilder() + .header("Proxy-Authorization", credentials) + .build() } } - } catch (e: IOException) { - emitter.onError(e) - } - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { + val client: OkHttpClient = builder.build() + val request: Request = Builder().url("https://www.example.com").head().build() + try { + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) throw IOException(response.message) + } + } catch (e: IOException) { + throw e + } + withContext(Dispatchers.Main) { txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_green)) val message = String.format("%s %s", "{fa-check}", context.getString(R.string.proxy_test_successful)) txtvMessage.text = message setTestRequired(false) - }, - { error: Throwable -> - error.printStackTrace() - txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_red)) - val message = String.format("%s %s: %s", "{fa-close}", context.getString(R.string.proxy_test_failed), error.message) - txtvMessage.text = message - setTestRequired(true) } - ) + } catch (e: Throwable) { + e.printStackTrace() + txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_red)) + val message = String.format("%s %s: %s", "{fa-close}", context.getString(R.string.proxy_test_failed), e.message) + txtvMessage.text = message + setTestRequired(true) + } + } + } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt index 81156bed..6e4597df 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt @@ -2,7 +2,11 @@ package ac.mdiq.podcini.ui.dialog import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.TimeDialogBinding -import ac.mdiq.podcini.util.event.playback.SleepTimerUpdatedEvent +import ac.mdiq.podcini.playback.PlaybackController +import ac.mdiq.podcini.playback.PlaybackController.Companion.disableSleepTimer +import ac.mdiq.podcini.playback.PlaybackController.Companion.extendSleepTimer +import ac.mdiq.podcini.playback.PlaybackController.Companion.setSleepTimer +import ac.mdiq.podcini.playback.service.PlaybackService import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo @@ -16,12 +20,9 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.setVibrate import ac.mdiq.podcini.preferences.SleepTimerPreferences.shakeToReset import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis import ac.mdiq.podcini.preferences.SleepTimerPreferences.vibrate -import ac.mdiq.podcini.playback.service.PlaybackService import ac.mdiq.podcini.util.Converter.getDurationStringLong -import ac.mdiq.podcini.playback.PlaybackController -import ac.mdiq.podcini.playback.PlaybackController.Companion.disableSleepTimer -import ac.mdiq.podcini.playback.PlaybackController.Companion.extendSleepTimer -import ac.mdiq.podcini.playback.PlaybackController.Companion.setSleepTimer +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Activity import android.app.Dialog import android.content.Context @@ -31,12 +32,12 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.* import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import java.util.* class SleepTimerDialog : DialogFragment() { @@ -56,13 +57,13 @@ class SleepTimerDialog : DialogFragment() { override fun loadMediaInfo() {} } controller.init() - EventBus.getDefault().register(this) + procFlowEvents() } @UnstableApi override fun onStop() { super.onStop() controller.release() - EventBus.getDefault().unregister(this) + } @UnstableApi override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -191,9 +192,18 @@ class SleepTimerDialog : DialogFragment() { chAutoEnable.text = text } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun timerUpdated(event: SleepTimerUpdatedEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.SleepTimerUpdatedEvent -> timerUpdated(event) + else -> {} + } + } + } + } + + fun timerUpdated(event: FlowEvent.SleepTimerUpdatedEvent) { timeDisplay.visibility = if (event.isOver || event.isCancelled) View.GONE else View.VISIBLE timeSetup.visibility = if (event.isOver || event.isCancelled) View.VISIBLE else View.GONE time.text = getDurationStringLong(event.getTimeLeft().toInt()) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/SubscriptionsFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/SubscriptionsFilterDialog.kt index 32aff926..07390b86 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/SubscriptionsFilterDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/SubscriptionsFilterDialog.kt @@ -1,5 +1,14 @@ package ac.mdiq.podcini.ui.dialog +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.FilterDialogBinding +import ac.mdiq.podcini.databinding.FilterDialogRowBinding +import ac.mdiq.podcini.feed.SubscriptionsFilterGroup +import ac.mdiq.podcini.preferences.UserPreferences +import ac.mdiq.podcini.preferences.UserPreferences.subscriptionsFilter +import ac.mdiq.podcini.storage.model.feed.SubscriptionsFilter +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Dialog import android.content.DialogInterface import android.os.Bundle @@ -13,15 +22,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButtonToggleGroup -import ac.mdiq.podcini.R -import ac.mdiq.podcini.feed.SubscriptionsFilterGroup -import ac.mdiq.podcini.databinding.FilterDialogBinding -import ac.mdiq.podcini.databinding.FilterDialogRowBinding -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent -import ac.mdiq.podcini.storage.model.feed.SubscriptionsFilter -import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.preferences.UserPreferences.subscriptionsFilter -import org.greenrobot.eventbus.EventBus import java.util.* class SubscriptionsFilterDialog : BottomSheetDialogFragment() { @@ -111,7 +111,7 @@ class SubscriptionsFilterDialog : BottomSheetDialogFragment() { private fun updateFilter(filterValues: Set) { val subscriptionsFilter = SubscriptionsFilter(filterValues.toTypedArray()) UserPreferences.subscriptionsFilter = subscriptionsFilter - EventBus.getDefault().post(UnreadItemsUpdateEvent()) + EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) } } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt index d64ea332..d696ab3e 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt @@ -7,7 +7,8 @@ import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.storage.model.feed.FeedPreferences import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter import ac.mdiq.podcini.ui.view.ItemOffsetDecoration -import ac.mdiq.podcini.util.event.FeedTagsChangedEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Dialog import android.content.DialogInterface import android.os.Bundle @@ -23,7 +24,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.greenrobot.eventbus.EventBus import java.io.Serializable class TagSettingsDialog : DialogFragment() { @@ -81,7 +81,7 @@ class TagSettingsDialog : DialogFragment() { addTag(binding.newTagEditText.text.toString().trim { it <= ' ' }) updatePreferencesTags(feedPreferencesList, commonTags) DBReader.buildTags() - EventBus.getDefault().post(FeedTagsChangedEvent()) + EventFlow.postEvent(FlowEvent.FeedTagsChangedEvent()) } dialog.setNegativeButton(R.string.cancel_label, null) return dialog.create() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt index afa79d84..c3956d3e 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt @@ -11,7 +11,8 @@ import ac.mdiq.podcini.preferences.UserPreferences.playbackSpeedArray import ac.mdiq.podcini.ui.view.ItemOffsetDecoration import ac.mdiq.podcini.ui.view.PlaybackSpeedSeekBar import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle import android.os.Handler import android.os.Looper @@ -21,15 +22,15 @@ import android.view.View import android.view.ViewGroup import android.widget.CompoundButton import androidx.annotation.OptIn +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import java.text.DecimalFormatSymbols import java.util.* @@ -57,7 +58,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { super.onStart() controller = object : PlaybackController(requireActivity()) { override fun loadMediaInfo() { - if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) + if (controller != null) updateSpeed(FlowEvent.SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) } override fun onPlaybackServiceConnected() { @@ -72,19 +73,29 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { } controller?.init() - EventBus.getDefault().register(this) - if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) + procFlowEvents() + if (controller != null) updateSpeed(FlowEvent.SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) } @UnstableApi override fun onStop() { super.onStop() controller?.release() controller = null - EventBus.getDefault().unregister(this) + } - @Subscribe(threadMode = ThreadMode.MAIN) - fun updateSpeed(event: SpeedChangedEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.SpeedChangedEvent -> updateSpeed(event) + else -> {} + } + } + } + } + + fun updateSpeed(event: FlowEvent.SpeedChangedEvent) { speedSeekBar.updateSpeed(event.newSpeed) addCurrentSpeedChip.text = String.format(Locale.getDefault(), "%1$.2f", event.newSpeed) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VideoModeDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VideoModeDialog.kt index c361b8ee..a282cb00 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VideoModeDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VideoModeDialog.kt @@ -1,15 +1,11 @@ package ac.mdiq.podcini.ui.dialog +import ac.mdiq.podcini.R +import ac.mdiq.podcini.preferences.UserPreferences.setVideoMode +import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode import android.content.Context import android.content.DialogInterface import com.google.android.material.dialog.MaterialAlertDialogBuilder -import ac.mdiq.podcini.R -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent -import ac.mdiq.podcini.preferences.UserPreferences.feedOrder -import ac.mdiq.podcini.preferences.UserPreferences.setFeedOrder -import ac.mdiq.podcini.preferences.UserPreferences.setVideoMode -import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode -import org.greenrobot.eventbus.EventBus object VideoModeDialog { fun showDialog(context: Context) { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt index 1f70bc39..21c20224 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AllEpisodesFragment.kt @@ -8,25 +8,26 @@ import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedItemFilter import ac.mdiq.podcini.storage.model.feed.SortOrder import ac.mdiq.podcini.ui.dialog.AllEpisodesFilterDialog -import ac.mdiq.podcini.ui.dialog.AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent import ac.mdiq.podcini.ui.dialog.ItemSortDialog import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.FeedListUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.annotation.OptIn +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.apache.commons.lang3.StringUtils -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe /** * Shows all episodes (possibly filtered by user). */ -class AllEpisodesFragment : BaseEpisodesListFragment() { +@UnstableApi class AllEpisodesFragment : BaseEpisodesListFragment() { @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { val root = super.onCreateView(inflater, container, savedInstanceState) @@ -42,6 +43,11 @@ class AllEpisodesFragment : BaseEpisodesListFragment() { return root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun loadData(): List { return DBReader.getEpisodes(0, page * EPISODES_PER_PAGE, getFilter(), allEpisodesSortOrder) } @@ -79,7 +85,7 @@ class AllEpisodesFragment : BaseEpisodesListFragment() { if (filter.contains(FeedItemFilter.IS_FAVORITE)) filter.remove(FeedItemFilter.IS_FAVORITE) else filter.add(FeedItemFilter.IS_FAVORITE) - onFilterChanged(AllEpisodesFilterChangedEvent(HashSet(filter))) + onFilterChanged(FlowEvent.AllEpisodesFilterChangedEvent(HashSet(filter))) return true } R.id.episodes_sort -> { @@ -90,8 +96,18 @@ class AllEpisodesFragment : BaseEpisodesListFragment() { } } - @Subscribe - fun onFilterChanged(event: AllEpisodesFilterChangedEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.AllEpisodesFilterChangedEvent -> onFilterChanged(event) + else -> {} + } + } + } + } + + fun onFilterChanged(event: FlowEvent.AllEpisodesFilterChangedEvent) { prefFilterAllEpisodes = StringUtils.join(event.filterValues, ",") updateFilterUi() page = 1 @@ -117,14 +133,15 @@ class AllEpisodesFragment : BaseEpisodesListFragment() { } override fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) { - if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG) + if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG + || ascending == SortOrder.PLAYED_DATE_OLD_NEW || ascending == SortOrder.COMPLETED_DATE_OLD_NEW) super.onAddItem(title, ascending, descending, ascendingIsDefault) } override fun onSelectionChanged() { super.onSelectionChanged() allEpisodesSortOrder = sortOrder - EventBus.getDefault().post(FeedListUpdateEvent(0)) + EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(0)) } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index c79d4d3c..e44f93f1 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -38,9 +38,8 @@ import ac.mdiq.podcini.util.ChapterUtils import ac.mdiq.podcini.util.Converter import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.TimeSpeedConverter -import ac.mdiq.podcini.util.event.FavoritesEvent -import ac.mdiq.podcini.util.event.PlayerErrorEvent -import ac.mdiq.podcini.util.event.playback.* +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Activity import android.content.Intent import android.os.Bundle @@ -57,6 +56,7 @@ import androidx.core.app.ShareCompat import androidx.core.text.HtmlCompat import androidx.fragment.app.Fragment import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import coil.imageLoader import coil.request.ErrorResult @@ -65,10 +65,10 @@ import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.elevation.SurfaceColors import io.reactivex.disposables.Disposable -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.text.DecimalFormat import java.text.NumberFormat import kotlin.math.max @@ -94,7 +94,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar private lateinit var cardViewSeek: CardView private lateinit var txtvSeek: TextView - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) private var controller: PlaybackController? = null // private var disposable: Disposable? = null private var seekedToChapterStart = false @@ -145,7 +145,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar cardViewSeek = binding.cardViewSeek txtvSeek = binding.txtvSeek - EventBus.getDefault().register(this) return binding.root } @@ -163,8 +162,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar _binding = null controller?.release() controller = null - scope.cancel() - EventBus.getDefault().unregister(this) +// scope.cancel() + Logd(TAG, "Fragment destroyed") } @@ -187,12 +186,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar // updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration)) // } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { - if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) - (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED - } - // private fun loadMediaInfo0(includingChapters: Boolean) { // Logd(TAG, "loadMediaInfo called") // @@ -226,7 +219,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar val theMedia = controller?.getMedia() ?: return if (currentMedia == null || theMedia.getIdentifier() != currentMedia?.getIdentifier() || (includingChapters && !theMedia.chaptersLoaded())) { Logd(TAG, "loadMediaInfo loading details ${theMedia.getIdentifier()} chapter: $includingChapters") - scope.launch { + lifecycleScope.launch { val media: Playable = withContext(Dispatchers.IO) { theMedia.apply { if (includingChapters) ChapterUtils.loadChapters(this, requireContext(), false) @@ -270,12 +263,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar setupOptionsMenu(currentMedia) } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) { - if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) retainInstance = true @@ -283,6 +270,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar override fun onStart() { super.onStart() + procFlowEvents() loadMediaInfo(false) } @@ -311,18 +299,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar // } // } - @Subscribe(threadMode = ThreadMode.MAIN) - fun favoritesChanged(event: FavoritesEvent?) { - loadMediaInfo(false) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun mediaPlayerError(event: PlayerErrorEvent) { - MediaPlayerErrorDialog.show(activity as Activity, event) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEvenStartPlay(event: StartPlayEvent) { + fun onEvenStartPlay(event: FlowEvent.StartPlayEvent) { Logd(TAG, "onEvenStartPlay ${event.item.title}") currentitem = event.item if (currentMedia?.getIdentifier() == null || currentitem!!.media?.getIdentifier() != currentMedia?.getIdentifier()) @@ -330,6 +307,25 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar (activity as MainActivity).setPlayerVisible(true) } + private fun procFlowEvents() { + lifecycleScope.launch { + Logd(TAG, "subscribing PositionFlowEvent") + EventFlow.events.collectLatest { event -> +// Logd(TAG, "PositionFlowEvent: ${event}") + when (event) { + is FlowEvent.PlaybackServiceEvent -> + if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) + (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + is FlowEvent.StartPlayEvent -> onEvenStartPlay(event) + is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event) + is FlowEvent.FavoritesEvent -> loadMediaInfo(false) + is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false) + else -> {} + } + } + } + } + override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { if (controller == null) return @@ -539,15 +535,26 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } } } - - EventBus.getDefault().register(this) return binding.root } @OptIn(UnstableApi::class) override fun onDestroyView() { super.onDestroyView() _binding = null - EventBus.getDefault().unregister(this) + } + + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> +// Logd(TAG, "PositionFlowEvent: ${event}") + when (event) { + is FlowEvent.PlaybackPositionEvent -> onPositionObserverUpdate(event) + is FlowEvent.SpeedChangedEvent -> updatePlaybackSpeedButton(event) + is FlowEvent.PlaybackServiceEvent -> onPlaybackServiceChanged(event) + else -> {} + } + } + } } @UnstableApi @@ -619,20 +626,18 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar if (controller == null) return@OnClickListener showTimeLeft = !showTimeLeft UserPreferences.setShowRemainTimeSetting(showTimeLeft) - onPositionObserverUpdate(PlaybackPositionEvent(controller!!.position, controller!!.duration)) + onPositionObserverUpdate(FlowEvent.PlaybackPositionEvent(controller!!.position, controller!!.duration)) }) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun updatePlaybackSpeedButton(event: SpeedChangedEvent) { + fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) { val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble()) txtvPlaybackSpeed.text = speedStr butPlaybackSpeed.setSpeed(event.newSpeed) } @UnstableApi - @Subscribe(threadMode = ThreadMode.MAIN) - fun onPositionObserverUpdate(event: PlaybackPositionEvent) { + fun onPositionObserverUpdate(event: FlowEvent.PlaybackPositionEvent) { if (controller == null || controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) return val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) @@ -668,11 +673,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { + fun onPlaybackServiceChanged(event: FlowEvent.PlaybackServiceEvent) { when (event.action) { - PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> (activity as MainActivity).setPlayerVisible(false) - PlaybackServiceEvent.Action.SERVICE_STARTED -> (activity as MainActivity).setPlayerVisible(true) + FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> (activity as MainActivity).setPlayerVisible(false) + FlowEvent.PlaybackServiceEvent.Action.SERVICE_STARTED -> (activity as MainActivity).setPlayerVisible(true) // PlaybackServiceEvent.Action.SERVICE_RESTARTED -> (activity as MainActivity).setPlayerVisible(true) } } @@ -685,12 +689,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar @OptIn(UnstableApi::class) override fun onStart() { super.onStart() + procFlowEvents() + txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) if (UserPreferences.speedforwardSpeed > 0.1f) txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed) else txtvSkip.visibility = View.GONE val media = controller?.getMedia() ?: return - updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) + updatePlaybackSpeedButton(FlowEvent.SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) } @UnstableApi @@ -717,7 +723,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar episodeTitle.text = media.getEpisodeTitle() (activity as MainActivity).setPlayerVisible(true) - onPositionObserverUpdate(PlaybackPositionEvent(media.getPosition(), media.getDuration())) + onPositionObserverUpdate(FlowEvent.PlaybackPositionEvent(media.getPosition(), media.getDuration())) val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media) val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/BaseEpisodesListFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/BaseEpisodesListFragment.kt index e5101c46..ed6f7041 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/BaseEpisodesListFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/BaseEpisodesListFragment.kt @@ -20,8 +20,8 @@ import ac.mdiq.podcini.ui.view.LiftOnScrollListener import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder import ac.mdiq.podcini.util.FeedItemUtil import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.* -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.DialogInterface import android.os.Bundle import android.util.Log @@ -31,6 +31,7 @@ import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.core.util.Pair import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator @@ -39,16 +40,17 @@ import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.snackbar.Snackbar import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialView -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Shows unread or recently published episodes */ -abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener { +@UnstableApi abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener { + @JvmField protected var page: Int = 1 protected var isLoadingMore: Boolean = false @@ -58,7 +60,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect var _binding: BaseEpisodesListFragmentBinding? = null protected val binding get() = _binding!! - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) lateinit var recyclerView: EpisodeItemListRecyclerView lateinit var emptyView: EmptyViewHandler @@ -120,19 +122,8 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect FeedUpdateManager.runOnceOrAsk(requireContext()) } - listAdapter = object : EpisodeItemListAdapter(activity as MainActivity) { - override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { - super.onCreateContextMenu(menu, v, menuInfo) -// if (!inActionMode()) { -// menu.findItem(R.id.multi_select).setVisible(true) -// } - MenuItemUtils.setOnClickListeners(menu) { item: MenuItem -> - this@BaseEpisodesListFragment.onContextItemSelected(item) - } - } - } - listAdapter.setOnSelectModeListener(this) - recyclerView.adapter = listAdapter + createListAdaptor() + progressBar = binding.progressBar progressBar.visibility = View.VISIBLE @@ -181,12 +172,31 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect true } - EventBus.getDefault().register(this) - loadItems() - return binding.root } + open fun createListAdaptor() { + listAdapter = object : EpisodeItemListAdapter(activity as MainActivity) { + override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) { + super.onCreateContextMenu(menu, v, menuInfo) +// if (!inActionMode()) { +// menu.findItem(R.id.multi_select).setVisible(true) +// } + MenuItemUtils.setOnClickListeners(menu) { item: MenuItem -> + this@BaseEpisodesListFragment.onContextItemSelected(item) + } + } + } + listAdapter.setOnSelectModeListener(this) + recyclerView.adapter = listAdapter + } + + override fun onStart() { + super.onStart() + procFlowEvents() + loadItems() + } + override fun onResume() { super.onResume() registerForContextMenu(recyclerView) @@ -198,10 +208,10 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect unregisterForContextMenu(recyclerView) } - override fun onStop() { - super.onStop() -// disposable?.dispose() - } +// override fun onStop() { +// super.onStop() +//// disposable?.dispose() +// } @UnstableApi override fun onOptionsItemSelected(item: MenuItem): Boolean { if (super.onOptionsItemSelected(item)) return true @@ -257,7 +267,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect // .subscribe({ listAdapter.endSelectMode() }, // { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - scope.launch { + lifecycleScope.launch { try { withContext(Dispatchers.IO) { handler.handleAction(listAdapter.selectedItems.filterIsInstance()) @@ -322,7 +332,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect // recyclerView.post { isLoadingMore = false } // }) - scope.launch { + lifecycleScope.launch { try { val data = withContext(Dispatchers.IO) { loadMoreData(page) @@ -348,8 +358,8 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect override fun onDestroyView() { super.onDestroyView() _binding = null - scope.cancel() - EventBus.getDefault().unregister(this) +// scope.cancel() + listAdapter.endSelectMode() } @@ -362,8 +372,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect speedDialView.visibility = View.GONE } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedItemEvent) { + fun onEventMainThread(event: FlowEvent.FeedItemEvent) { Logd(TAG, "onEventMainThread() called with FeedItemEvent event = [$event]") for (item in event.items) { val pos: Int = FeedItemUtil.indexOfItemWithId(episodes, item.id) @@ -377,8 +386,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent) { + fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) { // Log.d(TAG, "onEventMainThread() called with PlaybackPositionEvent event = [$event]") if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem) currentPlaying!!.notifyPlaybackPositionUpdated(event) @@ -395,7 +403,6 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect } } - @Subscribe(threadMode = ThreadMode.MAIN) fun onKeyUp(event: KeyEvent) { if (!isAdded || !isVisible || !isMenuVisible) return @@ -406,32 +413,39 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect } } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: EpisodeDownloadEvent) { + fun onEventMainThread(event: FlowEvent.EpisodeDownloadEvent) { for (downloadUrl in event.urls) { val pos: Int = FeedItemUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl) if (pos >= 0) listAdapter.notifyItemChangedCompat(pos) } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlayerStatusChanged(event: PlayerStatusEvent?) { - loadItems() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { - loadItems() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onFeedListChanged(event: FeedListUpdateEvent?) { - loadItems() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) { - refreshSwipeTelltale() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() + is FlowEvent.FeedListUpdateEvent, is FlowEvent.UnreadItemsUpdateEvent, is FlowEvent.PlayerStatusEvent -> loadItems() + is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event) + is FlowEvent.FeedItemEvent -> onEventMainThread(event) + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.EpisodeDownloadEvent -> onEventMainThread(event) + is FlowEvent.FeedUpdateRunningEvent -> onEventMainThread(event) + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.keyEvents.collectLatest { event -> + onKeyUp(event) + } + } } private fun refreshSwipeTelltale() { @@ -465,7 +479,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect // Log.e(TAG, Log.getStackTraceString(error)) // }) - scope.launch { + lifecycleScope.launch { try { val data = withContext(Dispatchers.IO) { Pair(loadData().toMutableList(), loadTotalItemCount()) @@ -502,11 +516,9 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect protected abstract fun getPrefName(): String - protected open fun updateToolbar() { - } + protected open fun updateToolbar() {} - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedUpdateRunningEvent) { + fun onEventMainThread(event: FlowEvent.FeedUpdateRunningEvent) { swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt index db34cefa..2ba21408 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt @@ -11,7 +11,8 @@ import ac.mdiq.podcini.ui.adapter.ChaptersListAdapter import ac.mdiq.podcini.util.ChapterUtils.getCurrentChapterIndex import ac.mdiq.podcini.util.ChapterUtils.loadChapters import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Dialog import android.content.DialogInterface import android.os.Bundle @@ -23,18 +24,15 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDialogFragment import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import io.reactivex.Maybe -import io.reactivex.MaybeEmitter -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @UnstableApi class ChaptersFragment : AppCompatDialogFragment() { @@ -46,7 +44,7 @@ class ChaptersFragment : AppCompatDialogFragment() { private lateinit var adapter: ChaptersListAdapter private var controller: PlaybackController? = null - private var disposable: Disposable? = null +// private var disposable: Disposable? = null private var focusedChapter = -1 private var media: Playable? = null @@ -100,27 +98,41 @@ class ChaptersFragment : AppCompatDialogFragment() { } } controller?.init() - EventBus.getDefault().register(this) - loadMediaInfo(false) return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + loadMediaInfo(false) + } + override fun onDestroyView() { super.onDestroyView() _binding = null controller?.release() controller = null - EventBus.getDefault().unregister(this) + } - override fun onStop() { - super.onStop() - disposable?.dispose() +// override fun onStop() { +// super.onStop() +//// disposable?.dispose() +// } + + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event) + else -> {} + } + } + } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent) { + fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) { updateChapterSelection(getCurrentChapter(media), false) adapter.notifyTimeChanged(event.position.toLong()) } @@ -132,19 +144,31 @@ class ChaptersFragment : AppCompatDialogFragment() { } private fun loadMediaInfo(forceRefresh: Boolean) { - disposable?.dispose() +// disposable?.dispose() - disposable = Maybe.create { emitter: MaybeEmitter -> - val media = controller!!.getMedia() - if (media != null) { - loadChapters(media, requireContext(), forceRefresh) - emitter.onSuccess(media) - } else emitter.onComplete() +// disposable = Maybe.create { emitter: MaybeEmitter -> +// val media = controller!!.getMedia() +// if (media != null) { +// loadChapters(media, requireContext(), forceRefresh) +// emitter.onSuccess(media) +// } else emitter.onComplete() +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ media: Any -> onMediaChanged(media as Playable) }, +// { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + + lifecycleScope.launch { + val media = withContext(Dispatchers.IO) { + val media_ = controller!!.getMedia() + if (media_ != null) loadChapters(media_, requireContext(), forceRefresh) + media_ + } + onMediaChanged(media as Playable) + }.invokeOnCompletion { throwable -> + if (throwable!= null) Logd(TAG, Log.getStackTraceString(throwable)) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ media: Any -> onMediaChanged(media as Playable) }, - { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + } private fun onMediaChanged(media: Playable) { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/DiscoveryFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/DiscoveryFragment.kt index 8a398c06..1ad8cde3 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/DiscoveryFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/DiscoveryFragment.kt @@ -10,7 +10,8 @@ import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.adapter.OnlineFeedsAdapter import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.DiscoveryDefaultUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences @@ -26,12 +27,12 @@ import android.widget.AdapterView.OnItemClickListener import androidx.annotation.OptIn import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.MaterialAutoCompleteTextView import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus import java.util.* /** @@ -60,7 +61,7 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener { private var searchResults: List? = null private var topList: List? = null - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) // private var disposable: Disposable? = null private var countryCode: String? = "US" private var hidden = false @@ -135,7 +136,7 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener { override fun onDestroy() { super.onDestroy() _binding = null - scope.cancel() +// scope.cancel() // disposable?.dispose() adapter = null @@ -194,7 +195,7 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener { // butRetry.visibility = View.VISIBLE // }) - scope.launch { + lifecycleScope.launch { try { val podcasts = withContext(Dispatchers.IO) { loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, DBReader.getFeedList()) @@ -226,7 +227,7 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener { hidden = item.isChecked prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply() - EventBus.getDefault().post(DiscoveryDefaultUpdateEvent()) + EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent()) loadToplist(countryCode) return true } @@ -280,7 +281,7 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener { prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply() prefs.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply() - EventBus.getDefault().post(DiscoveryDefaultUpdateEvent()) + EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent()) loadToplist(countryCode) } builder.setNegativeButton(R.string.cancel_label, null) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt index 8f50e93c..06657e60 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt @@ -9,18 +9,21 @@ import ac.mdiq.podcini.ui.adapter.DownloadLogAdapter import ac.mdiq.podcini.ui.dialog.DownloadLogDetailsDialog import ac.mdiq.podcini.ui.view.EmptyViewHandler import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.DownloadLogEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle import android.util.Log import android.view.* import android.widget.AdapterView import android.widget.AdapterView.OnItemClickListener import androidx.appcompat.widget.Toolbar +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Shows the download log @@ -33,11 +36,11 @@ class DownloadLogFragment : BottomSheetDialogFragment(), OnItemClickListener, To private var downloadLog: List = ArrayList() // private var disposable: Disposable? = null - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) override fun onStop() { super.onStop() - scope.cancel() +// scope.cancel() // disposable?.dispose() } @@ -57,14 +60,17 @@ class DownloadLogFragment : BottomSheetDialogFragment(), OnItemClickListener, To binding.list.adapter = adapter binding.list.onItemClickListener = this binding.list.isNestedScrollingEnabled = true - EventBus.getDefault().register(this) loadDownloadLog() return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun onDestroyView() { - EventBus.getDefault().unregister(this) super.onDestroyView() _binding = null } @@ -74,9 +80,15 @@ class DownloadLogFragment : BottomSheetDialogFragment(), OnItemClickListener, To if (item is DownloadResult) DownloadLogDetailsDialog(requireContext(), item).show() } - @Subscribe - fun onDownloadLogChanged(event: DownloadLogEvent?) { - loadDownloadLog() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.DownloadLogEvent -> loadDownloadLog() + else -> {} + } + } + } } override fun onPrepareOptionsMenu(menu: Menu) { @@ -107,7 +119,7 @@ class DownloadLogFragment : BottomSheetDialogFragment(), OnItemClickListener, To // } // }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - scope.launch { + lifecycleScope.launch { try { val result = withContext(Dispatchers.IO) { DBReader.getDownloadLog() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index 952497dc..247e3af8 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -25,8 +25,8 @@ import ac.mdiq.podcini.ui.view.LiftOnScrollListener import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder import ac.mdiq.podcini.util.FeedItemUtil import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.* -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle import android.util.Log import android.view.* @@ -34,6 +34,7 @@ import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator @@ -41,19 +42,16 @@ import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.snackbar.Snackbar import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialView -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import java.util.* /** * Displays all completed downloads and provides a button to delete them. */ -class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener { +@UnstableApi class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener { private var _binding: SimpleListFragmentBinding? = null private val binding get() = _binding!! @@ -148,20 +146,23 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To DownloadLogFragment().show(childFragmentManager, null) addEmptyView() - EventBus.getDefault().register(this) - - loadItems() return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + loadItems() + } + override fun onSaveInstanceState(outState: Bundle) { outState.putBoolean(KEY_UP_ARROW, displayUpArrow) super.onSaveInstanceState(outState) } override fun onDestroyView() { - EventBus.getDefault().unregister(this) + _binding = null adapter.endSelectMode() toolbar.setOnMenuItemClickListener(null) @@ -193,8 +194,7 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To } } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: EpisodeDownloadEvent) { + fun onEventMainThread(event: FlowEvent.EpisodeDownloadEvent) { val newRunningDownloads: MutableSet = HashSet() for (url in event.urls) { if (DownloadServiceInterface.get()?.isDownloadingEpisode(url) == true) newRunningDownloads.add(url) @@ -210,6 +210,28 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To } } + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.FeedItemEvent -> onEventMainThread(event) + is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event) + is FlowEvent.PlayerStatusEvent, is FlowEvent.DownloadLogEvent, is FlowEvent.UnreadItemsUpdateEvent -> loadItems() + is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.EpisodeDownloadEvent -> onEventMainThread(event) + else -> {} + } + } + } + } + override fun onContextItemSelected(item: MenuItem): Boolean { val selectedItem: FeedItem? = adapter.longPressedItem if (selectedItem == null) { @@ -229,8 +251,7 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To emptyView.attachToRecyclerView(recyclerView) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedItemEvent) { + fun onEventMainThread(event: FlowEvent.FeedItemEvent) { Logd(TAG, "onEventMainThread() called with: event = [$event]") var i = 0 @@ -258,8 +279,7 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To refreshInfoBar() } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent) { + fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) { // Log.d(TAG, "onEventMainThread() called with PlaybackPositionEvent event = [$event]") if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem) currentPlaying!!.notifyPlaybackPositionUpdated(event) @@ -277,26 +297,6 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To refreshInfoBar() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlayerStatusChanged(event: PlayerStatusEvent?) { - loadItems() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onDownloadLogChanged(event: DownloadLogEvent?) { - loadItems() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { - loadItems() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) { - refreshSwipeTelltale() - } - private fun refreshSwipeTelltale() { if (swipeActions.actions?.left != null) binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon()) if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon()) @@ -337,8 +337,8 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To // Log.e(TAG, Log.getStackTraceString(error)) // }) - val scope = CoroutineScope(Dispatchers.Main) - scope.launch { +// val scope = CoroutineScope(Dispatchers.Main) + lifecycleScope.launch { try { val result = withContext(Dispatchers.IO) { val sortOrder: SortOrder? = UserPreferences.downloadsSortedOrder @@ -423,7 +423,9 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To } override fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) { - if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG || ascending == SortOrder.EPISODE_TITLE_A_Z + if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.PLAYED_DATE_OLD_NEW + || ascending == SortOrder.COMPLETED_DATE_OLD_NEW + || ascending == SortOrder.DURATION_SHORT_LONG || ascending == SortOrder.EPISODE_TITLE_A_Z || ascending == SortOrder.SIZE_SMALL_LARGE || ascending == SortOrder.FEED_TITLE_A_Z) { super.onAddItem(title, ascending, descending, ascendingIsDefault) } @@ -432,7 +434,7 @@ class DownloadsFragment : Fragment(), SelectableAdapter.OnSelectModeListener, To override fun onSelectionChanged() { super.onSelectionChanged() UserPreferences.downloadsSortedOrder = sortOrder - EventBus.getDefault().post(DownloadLogEvent.listUpdated()) + EventFlow.postEvent(FlowEvent.DownloadLogEvent()) } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index f6f61867..a9ff5a7c 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -22,10 +22,8 @@ import ac.mdiq.podcini.util.Converter import ac.mdiq.podcini.util.DateFormatter import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.PlaybackStatus -import ac.mdiq.podcini.util.event.EpisodeDownloadEvent -import ac.mdiq.podcini.util.event.FeedItemEvent -import ac.mdiq.podcini.util.event.PlayerStatusEvent -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.os.Build import android.os.Bundle import android.text.Layout @@ -41,6 +39,7 @@ import androidx.appcompat.widget.Toolbar import androidx.core.app.ShareCompat import androidx.core.text.HtmlCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import coil.imageLoader import coil.request.ErrorResult @@ -51,20 +50,17 @@ import com.skydoves.balloon.ArrowOrientation import com.skydoves.balloon.ArrowOrientationRules import com.skydoves.balloon.Balloon import com.skydoves.balloon.BalloonAnimation -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import java.util.* import kotlin.math.max /** * Displays information about an Episode (FeedItem) and actions. */ -class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { +@UnstableApi class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { private var _binding: EpisodeInfoFragmentBinding? = null private val binding get() = _binding!! @@ -172,7 +168,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } }) - EventBus.getDefault().register(this) controller = object : PlaybackController(requireActivity()) { override fun loadMediaInfo() { // Do nothing @@ -184,6 +179,11 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + @OptIn(UnstableApi::class) private fun showOnDemandConfigBalloon(offerStreaming: Boolean) { val isLocaleRtl = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL) val balloon: Balloon = Balloon.Builder(requireContext()) @@ -208,7 +208,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { positiveButton.setOnClickListener { UserPreferences.isStreamOverDownload = offerStreaming // Update all visible lists to reflect new streaming action button - EventBus.getDefault().post(UnreadItemsUpdateEvent()) + EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent()) (activity as MainActivity).showSnackbarAbovePlayer(R.string.on_demand_config_setting_changed, Snackbar.LENGTH_SHORT) balloon.dismiss() } @@ -256,7 +256,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { super.onDestroyView() Logd(TAG, "onDestroyView") _binding = null - EventBus.getDefault().unregister(this) + controller?.release() // disposable?.dispose() root.removeView(webvDescription) @@ -382,8 +382,28 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { (activity as MainActivity).loadChildFragment(fragment) } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedItemEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.FeedItemEvent -> onEventMainThread(event) + is FlowEvent.PlayerStatusEvent -> updateButtons() + is FlowEvent.UnreadItemsUpdateEvent -> load() + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.EpisodeDownloadEvent -> onEventMainThread(event) + else -> {} + } + } + } + } + + fun onEventMainThread(event: FlowEvent.FeedItemEvent) { Logd(TAG, "onEventMainThread() called with: event = [$event]") if (this.item == null) return for (item in event.items) { @@ -394,23 +414,12 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } } - @UnstableApi @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: EpisodeDownloadEvent) { + fun onEventMainThread(event: FlowEvent.EpisodeDownloadEvent) { if (item == null || item!!.media == null) return if (!event.urls.contains(item!!.media!!.download_url)) return if (itemsLoaded && activity != null) updateButtons() } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlayerStatusChanged(event: PlayerStatusEvent?) { - updateButtons() - } - - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { - load() - } - // @UnstableApi private fun load0() { // disposable?.dispose() // if (!itemsLoaded) progbarLoading.visibility = View.VISIBLE @@ -434,8 +443,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { if (!itemsLoaded) progbarLoading.visibility = View.VISIBLE Logd(TAG, "load() called") - val scope = CoroutineScope(Dispatchers.Main) - scope.launch { +// val scope = CoroutineScope(Dispatchers.Main) + lifecycleScope.launch { try { val result = withContext(Dispatchers.IO) { val feedItem = item diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodesListFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalEpisodesListFragment.kt similarity index 85% rename from app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodesListFragment.kt rename to app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalEpisodesListFragment.kt index e53d555e..989a56df 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodesListFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalEpisodesListFragment.kt @@ -3,22 +3,25 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedItemFilter -import ac.mdiq.podcini.ui.dialog.AllEpisodesFilterDialog.AllEpisodesFilterChangedEvent import ac.mdiq.podcini.util.Logd +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.annotation.OptIn +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import org.greenrobot.eventbus.Subscribe +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import kotlin.math.min /** * Shows all episodes (possibly filtered by user). */ -class EpisodesListFragment : BaseEpisodesListFragment() { +@UnstableApi class ExternalEpisodesListFragment : BaseEpisodesListFragment() { private val episodeList: MutableList = mutableListOf() @@ -40,6 +43,11 @@ class EpisodesListFragment : BaseEpisodesListFragment() { return root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + fun setEpisodes(episodeList_: MutableList) { episodeList.clear() episodeList.addAll(episodeList_) @@ -94,12 +102,15 @@ class EpisodesListFragment : BaseEpisodesListFragment() { } } - @Subscribe - fun onFilterChanged(event: AllEpisodesFilterChangedEvent) { -// prefFilterAllEpisodes = StringUtils.join(event.filterValues, ",") -// updateFilterUi() - page = 1 -// loadItems() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.AllEpisodesFilterChangedEvent -> page = 1 + else -> {} + } + } + } } private fun updateFilterUi() { @@ -143,8 +154,8 @@ class EpisodesListFragment : BaseEpisodesListFragment() { const val EXTRA_EPISODES: String = "episodes_list" @JvmStatic - fun newInstance(episodes: MutableList): EpisodesListFragment { - val i = EpisodesListFragment() + fun newInstance(episodes: MutableList): ExternalEpisodesListFragment { + val i = ExternalEpisodesListFragment() i.setEpisodes(episodes) return i } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt index 0874ec38..b0a32a74 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedInfoFragment.kt @@ -37,6 +37,7 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.Toolbar import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import coil.load import com.google.android.material.appbar.AppBarLayout @@ -285,8 +286,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { // .subscribe({ (activity as MainActivity).showSnackbarAbovePlayer(string.ok, Snackbar.LENGTH_SHORT) }, // { error: Throwable -> (activity as MainActivity).showSnackbarAbovePlayer(error.localizedMessage, Snackbar.LENGTH_LONG) }) - val scope = CoroutineScope(Dispatchers.Main) - scope.launch { +// val scope = CoroutineScope(Dispatchers.Main) + lifecycleScope.launch { try { withContext(Dispatchers.IO) { requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedItemlistFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedItemlistFragment.kt index bad97254..c5f84161 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedItemlistFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedItemlistFragment.kt @@ -3,7 +3,6 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.FeedItemListFragmentBinding import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding -import ac.mdiq.podcini.feed.FeedEvent import ac.mdiq.podcini.net.download.FeedUpdateManager import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.storage.DBReader @@ -25,8 +24,8 @@ import ac.mdiq.podcini.ui.utils.MoreContentListFooterUtil import ac.mdiq.podcini.ui.view.ToolbarIconTintManager import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder import ac.mdiq.podcini.util.* -import ac.mdiq.podcini.util.event.* -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.app.Activity import android.content.Context import android.content.res.Configuration @@ -40,6 +39,7 @@ import android.widget.Toast import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.RecyclerView import coil.load @@ -48,17 +48,15 @@ import com.joanzapata.iconify.Iconify import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialView import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collectLatest import org.apache.commons.lang3.StringUtils -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import java.util.concurrent.ExecutionException import java.util.concurrent.Semaphore /** * Displays a list of FeedItems. */ -class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolbar.OnMenuItemClickListener, +@UnstableApi class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolbar.OnMenuItemClickListener, SelectableAdapter.OnSelectModeListener { private var _binding: FeedItemListFragmentBinding? = null @@ -79,7 +77,7 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba // private var disposable: Disposable? = null private val ioScope = CoroutineScope(Dispatchers.IO) - private val scope = CoroutineScope(Dispatchers.Main) +// private val scope = CoroutineScope(Dispatchers.Main) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -151,14 +149,12 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba } }) - EventBus.getDefault().register(this) - binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance)) binding.swipeRefresh.setOnRefreshListener { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) } - loadItems() +// loadItems() // Init action UI (via a FAB Speed Dial) speedDialBinding.fabSD.overlayLayout = speedDialBinding.fabSDOverlay @@ -184,6 +180,12 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + loadItems() + } + private val semaphore = Semaphore(0) private fun initializeTTS(context: Context) { Logd(TAG, "starting TTS") @@ -207,10 +209,10 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba super.onDestroyView() _binding = null _speedDialBinding = null - EventBus.getDefault().unregister(this) + // disposable?.dispose() ioScope.cancel() - scope.cancel() +// scope.cancel() adapter.endSelectMode() tts?.stop() @@ -298,14 +300,12 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onEvent(event: FeedEvent) { + fun onEvent(event: FlowEvent.FeedEvent) { Logd(TAG, "onEvent() called with: event = [$event]") if (event.feedId == feedID) loadItems() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedItemEvent) { + fun onEventMainThread(event: FlowEvent.FeedItemEvent) { Logd(TAG, "onEventMainThread() called with FeedItemEvent event = [$event]") if (feed == null || feed!!.items.isEmpty()) return @@ -323,8 +323,7 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba } } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: EpisodeDownloadEvent) { + fun onEventMainThread(event: FlowEvent.EpisodeDownloadEvent) { Logd(TAG, "onEventMainThread() called with EpisodeDownloadEvent event = [$event]") if (feed == null || feed!!.items.isEmpty()) return @@ -334,8 +333,7 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent) { + fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) { // Log.d(TAG, "onEventMainThread() called with PlaybackPositionEvent event = [$event]") if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem) currentPlaying!!.notifyPlaybackPositionUpdated(event) else { @@ -351,17 +349,39 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun favoritesChanged(event: FavoritesEvent?) { - Logd(TAG, "favoritesChanged called") - loadItems() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.QueueEvent -> loadItems() + is FlowEvent.FavoritesEvent -> loadItems() + is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event) + is FlowEvent.FeedItemEvent -> onEventMainThread(event) + is FlowEvent.FeedEvent -> onEvent(event) + is FlowEvent.PlayerStatusEvent -> loadItems() + is FlowEvent.UnreadItemsUpdateEvent -> loadItems() + is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event) + is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.EpisodeDownloadEvent -> onEventMainThread(event) + is FlowEvent.FeedUpdateRunningEvent -> onEventMainThread(event) + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.keyEvents.collectLatest { event -> + onKeyUp(event) + } + } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onQueueChanged(event: QueueEvent?) { - Logd(TAG, "onQueueChanged called") - loadItems() - } override fun onStartSelectMode() { swipeActions.detach() @@ -383,33 +403,14 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba swipeActions.attachTo(binding.recyclerView) } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlayerStatusChanged(event: PlayerStatusEvent?) { - Logd(TAG, "onPlayerStatusChanged called") - loadItems() - } - - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { - Logd(TAG, "onUnreadItemsChanged called") - loadItems() - } - - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onFeedListChanged(event: FeedListUpdateEvent) { + fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent) { if (feed != null && event.contains(feed!!)) { Logd(TAG, "onFeedListChanged called") loadItems() } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) { - refreshSwipeTelltale() - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedUpdateRunningEvent) { + fun onEventMainThread(event: FlowEvent.FeedUpdateRunningEvent) { nextPageLoader.setLoadingState(event.isFeedUpdateRunning) if (!event.isFeedUpdateRunning) nextPageLoader.root.visibility = View.GONE binding.swipeRefresh.isRefreshing = event.isFeedUpdateRunning @@ -486,7 +487,7 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba // { error: Throwable -> error.printStackTrace() }, // { DownloadLogFragment().show(childFragmentManager, null) }) - scope.launch { + lifecycleScope.launch { val downloadResult = withContext(Dispatchers.IO) { val feedDownloadLog: List = DBReader.getFeedDownloadLog(feedID) if (feedDownloadLog.isEmpty() || feedDownloadLog[0].isSuccessful) null else feedDownloadLog[0] @@ -558,7 +559,7 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba // Log.e(TAG, Log.getStackTraceString(error)) // }) - scope.launch { + lifecycleScope.launch { try { feed = withContext(Dispatchers.IO) { val feed_ = loadData() @@ -616,7 +617,6 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba return feed } - @Subscribe(threadMode = ThreadMode.MAIN) fun onKeyUp(event: KeyEvent) { if (!isAdded || !isVisible || !isMenuVisible) return @@ -648,7 +648,8 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba } override fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) { - if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG || ascending == SortOrder.RANDOM + if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.PLAYED_DATE_OLD_NEW || ascending == SortOrder.COMPLETED_DATE_OLD_NEW + || ascending == SortOrder.DURATION_SHORT_LONG || ascending == SortOrder.RANDOM || ascending == SortOrder.EPISODE_TITLE_A_Z || (requireArguments().getBoolean(ARG_FEED_IS_LOCAL) && ascending == SortOrder.EPISODE_FILENAME_A_Z)) { super.onAddItem(title, ascending, descending, ascendingIsDefault) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index b1371a79..d462772e 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -17,9 +17,8 @@ import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog import ac.mdiq.podcini.ui.dialog.FeedPreferenceSkipDialog import ac.mdiq.podcini.ui.dialog.TagSettingsDialog import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.settings.SkipIntroEndingChangedEvent -import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent -import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.DialogInterface import android.content.Intent import android.net.Uri @@ -34,6 +33,7 @@ import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.OptIn import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.preference.ListPreference import androidx.preference.Preference @@ -42,7 +42,6 @@ import androidx.preference.SwitchPreferenceCompat import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus import java.util.* import java.util.concurrent.ExecutionException @@ -50,7 +49,7 @@ class FeedSettingsFragment : Fragment() { private var _binding: FeedsettingsBinding? = null private val binding get() = _binding!! - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) // private var disposable: Disposable? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -77,7 +76,7 @@ class FeedSettingsFragment : Fragment() { // { error: Throwable? -> Logd(TAG, Log.getStackTraceString(error)) }, // {}) - scope.launch { + lifecycleScope.launch { val feed = withContext(Dispatchers.IO) { DBReader.getFeed(feedId) } @@ -98,13 +97,13 @@ class FeedSettingsFragment : Fragment() { override fun onDestroyView() { super.onDestroyView() _binding = null - scope.cancel() +// scope.cancel() // disposable?.dispose() } class FeedSettingsPreferenceFragment : PreferenceFragmentCompat() { private var feed: Feed? = null - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) // private var disposable: Disposable? = null private var feedPreferences: FeedPreferences? = null @@ -173,7 +172,7 @@ class FeedSettingsFragment : Fragment() { // findPreference(PREF_SCREEN)!!.isVisible = true // }, { error: Throwable? -> Logd(TAG, Log.getStackTraceString(error)) }, {}) - scope.launch { + lifecycleScope.launch { feed = withContext(Dispatchers.IO) { DBReader.getFeed(feedId) } @@ -212,7 +211,7 @@ class FeedSettingsFragment : Fragment() { override fun onDestroy() { super.onDestroy() - scope.cancel() +// scope.cancel() // disposable?.dispose() } @@ -224,7 +223,7 @@ class FeedSettingsFragment : Fragment() { feedPreferences!!.feedSkipIntro = skipIntro feedPreferences!!.feedSkipEnding = skipEnding DBWriter.persistFeedPreferences(feedPreferences!!) - EventBus.getDefault().post(SkipIntroEndingChangedEvent(feedPreferences!!.feedSkipIntro, feedPreferences!!.feedSkipEnding, feed!!.id)) + EventFlow.postEvent(FlowEvent.SkipIntroEndingChangedEvent(feedPreferences!!.feedSkipIntro, feedPreferences!!.feedSkipEnding, feed!!.id)) } }.show() false @@ -256,7 +255,7 @@ class FeedSettingsFragment : Fragment() { else viewBinding.seekBar.currentSpeed feedPreferences!!.feedPlaybackSpeed = newSpeed if (feedPreferences != null) DBWriter.persistFeedPreferences(feedPreferences!!) - EventBus.getDefault().post(SpeedPresetChangedEvent(feedPreferences!!.feedPlaybackSpeed, feed!!.id)) + EventFlow.postEvent(FlowEvent.SpeedPresetChangedEvent(feedPreferences!!.feedPlaybackSpeed, feed!!.id)) } .setNegativeButton(R.string.cancel_label, null) .show() @@ -352,7 +351,7 @@ class FeedSettingsFragment : Fragment() { DBWriter.persistFeedPreferences(feedPreferences!!) updateVolumeAdaptationValue() if (feed != null && feedPreferences!!.volumeAdaptionSetting != null) - EventBus.getDefault().post(VolumeAdaptionChangedEvent(feedPreferences!!.volumeAdaptionSetting!!, feed!!.id)) + EventFlow.postEvent(FlowEvent.VolumeAdaptionChangedEvent(feedPreferences!!.volumeAdaptionSetting!!, feed!!.id)) false } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt index 1176e1f2..513a9065 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/NavDrawerFragment.kt @@ -15,9 +15,8 @@ import ac.mdiq.podcini.ui.dialog.SubscriptionsFilterDialog import ac.mdiq.podcini.ui.statistics.StatisticsFragment import ac.mdiq.podcini.ui.utils.ThemeUtils import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.FeedListUpdateEvent -import ac.mdiq.podcini.util.event.QueueEvent -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.R.attr import android.app.Activity import android.content.Context @@ -38,16 +37,17 @@ import androidx.core.graphics.Insets import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.apache.commons.lang3.StringUtils -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import kotlin.math.max class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChangeListener { @@ -56,7 +56,7 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange private var navDrawerData: NavDrawerData? = null private var flatItemList: List? = null - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) private lateinit var navAdapter: NavListAdapter @@ -117,33 +117,35 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - EventBus.getDefault().register(this) + procFlowEvents() } override fun onDestroyView() { super.onDestroyView() _binding = null - EventBus.getDefault().unregister(this) - scope.cancel() + +// scope.cancel() requireContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE).unregisterOnSharedPreferenceChangeListener(this) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { - loadData() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.UnreadItemsUpdateEvent, is FlowEvent.FeedListUpdateEvent -> loadData() + is FlowEvent.QueueEvent -> onQueueChanged(event) + else -> {} + } + } + } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onFeedListChanged(event: FeedListUpdateEvent?) { - loadData() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onQueueChanged(event: QueueEvent) { + fun onQueueChanged(event: FlowEvent.QueueEvent) { Logd(TAG, "onQueueChanged($event)") // we are only interested in the number of queue items, not download status or position - if (event.action == QueueEvent.Action.DELETED_MEDIA || event.action == QueueEvent.Action.SORTED || event.action == QueueEvent.Action.MOVED) return - + if (event.action == FlowEvent.QueueEvent.Action.DELETED_MEDIA + || event.action == FlowEvent.QueueEvent.Action.SORTED + || event.action == FlowEvent.QueueEvent.Action.MOVED) return loadData() } @@ -252,7 +254,7 @@ class NavDrawerFragment : Fragment(), SharedPreferences.OnSharedPreferenceChange } private fun loadData() { - scope.launch { + lifecycleScope.launch { try { val result = withContext(Dispatchers.IO) { val data: NavDrawerData = DBReader.getNavDrawerData(UserPreferences.subscriptionsFilter) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt index 64449b51..2eddd094 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt @@ -27,8 +27,8 @@ import ac.mdiq.podcini.ui.dialog.AuthenticationDialog import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr import ac.mdiq.podcini.util.DownloadErrorLabel.from import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.EpisodeDownloadEvent -import ac.mdiq.podcini.util.event.FeedListUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.syndication.FeedDiscoverer import ac.mdiq.podcini.util.syndication.HtmlToPlainText import android.app.Dialog @@ -50,19 +50,17 @@ import android.widget.Toast import androidx.annotation.OptIn import androidx.annotation.UiThread import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import coil.load import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode import org.jsoup.Jsoup import java.io.File import java.io.IOException @@ -79,7 +77,7 @@ import kotlin.concurrent.Volatile * If the feed cannot be downloaded or parsed, an error dialog will be displayed * and the activity will finish as soon as the error dialog is closed. */ -class OnlineFeedViewFragment : Fragment() { +@OptIn(UnstableApi::class) class OnlineFeedViewFragment : Fragment() { private var _binding: OnlineFeedviewFragmentBinding? = null private val binding get() = _binding!! @@ -98,7 +96,7 @@ class OnlineFeedViewFragment : Fragment() { private var dialog: Dialog? = null - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) private var download: Disposable? = null private var parser: Disposable? = null @@ -160,13 +158,13 @@ class OnlineFeedViewFragment : Fragment() { override fun onStart() { super.onStart() isPaused = false - EventBus.getDefault().register(this) + procFlowEvents() } override fun onStop() { super.onStop() isPaused = true - EventBus.getDefault().unregister(this) + if (downloader != null && !downloader!!.isFinished) downloader!!.cancel() if (dialog != null && dialog!!.isShowing) dialog!!.dismiss() } @@ -294,7 +292,7 @@ class OnlineFeedViewFragment : Fragment() { // .subscribe({ status: DownloadResult? -> if (request.destination != null) checkDownloadResult(status, request.destination) }, // { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - scope.launch { + lifecycleScope.launch { try { val status = withContext(Dispatchers.IO) { feeds = DBReader.getFeedList() @@ -329,8 +327,26 @@ class OnlineFeedViewFragment : Fragment() { } } - @UnstableApi @Subscribe - fun onFeedListChanged(event: FeedListUpdateEvent?) { + @OptIn(UnstableApi::class) private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event) + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.EpisodeDownloadEvent -> handleUpdatedFeedStatus() + else -> {} + } + } + } + } + + fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent) { // updater = Observable.fromCallable { DBReader.getFeedList() } // .subscribeOn(Schedulers.io()) // .observeOn(AndroidSchedulers.mainThread()) @@ -340,7 +356,7 @@ class OnlineFeedViewFragment : Fragment() { // handleUpdatedFeedStatus() // }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) } // ) - scope.launch { + lifecycleScope.launch { try { val feeds = withContext(Dispatchers.IO) { DBReader.getFeedList() @@ -357,11 +373,6 @@ class OnlineFeedViewFragment : Fragment() { } - @UnstableApi @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: EpisodeDownloadEvent?) { - handleUpdatedFeedStatus() - } - @OptIn(UnstableApi::class) private fun parseFeed(destination: String) { Logd(TAG, "Parsing feed") // parser = Maybe.fromCallable { doParseFeed(destination) } @@ -381,7 +392,7 @@ class OnlineFeedViewFragment : Fragment() { // Logd(TAG, "Feed parser exception: " + Log.getStackTraceString(error)) // } // }) - scope.launch { + lifecycleScope.launch { try { val result = withContext(Dispatchers.Default) { doParseFeed(destination) @@ -550,7 +561,7 @@ class OnlineFeedViewFragment : Fragment() { for (i in 0.. this@PlaybackHistoryFragment.onContextItemSelected(item) } + } + } + listAdapter.setOnSelectModeListener(this) + recyclerView.adapter = listAdapter + } + + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun getFilter(): FeedItemFilter { return FeedItemFilter.unfiltered() } + @OptIn(UnstableApi::class) override fun onMenuItemClick(item: MenuItem): Boolean { if (super.onOptionsItemSelected(item)) return true - - if (item.itemId == R.id.clear_history_item) { - val conDialog: ConfirmationDialog = object : ConfirmationDialog(requireContext(), R.string.clear_history_label, R.string.clear_playback_history_msg) { - override fun onConfirmButtonPressed(dialog: DialogInterface) { - dialog.dismiss() - DBWriter.clearPlaybackHistory() - } + when (item.itemId) { + R.id.episodes_sort -> { + HistorySortDialog().show(childFragmentManager.beginTransaction(), "SortDialog") + return true + } + R.id.filter_items -> { + val dialog = object: DatesFilterDialog(requireContext(), 0L) { + override fun initParams() { + val calendar = Calendar.getInstance() + calendar.add(Calendar.YEAR, -1) // subtract 1 year + timeFilterFrom = calendar.timeInMillis + showMarkPlayed = false + } + override fun callback(timeFilterFrom: Long, timeFilterTo: Long, includeMarkedAsPlayed: Boolean) { + EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder, timeFilterFrom, timeFilterTo)) + } + } + dialog.show() + return true + } + R.id.clear_history_item -> { + val conDialog: ConfirmationDialog = object : ConfirmationDialog(requireContext(), R.string.clear_history_label, R.string.clear_playback_history_msg) { + override fun onConfirmButtonPressed(dialog: DialogInterface) { + dialog.dismiss() + DBWriter.clearPlaybackHistory() + } + } + conDialog.createNewDialog().show() + return true } - conDialog.createNewDialog().show() - return true } return false } override fun updateToolbar() { // Not calling super, as we do not have a refresh button that could be updated + toolbar.menu.findItem(R.id.episodes_sort).setVisible(episodes.isNotEmpty()) + toolbar.menu.findItem(R.id.filter_items).setVisible(episodes.isNotEmpty()) toolbar.menu.findItem(R.id.clear_history_item).setVisible(episodes.isNotEmpty()) } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onHistoryUpdated(event: PlaybackHistoryEvent?) { - loadItems() - updateToolbar() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.HistoryEvent -> { + sortOrder = event.sortOrder + if (event.startDate > 0) startDate = event.startDate + endDate = event.endDate + loadItems() + updateToolbar() + } + else -> {} + } + } + } } override fun loadData(): List { - return DBReader.getPlaybackHistory(0, page * EPISODES_PER_PAGE) + val hList = DBReader.getPlaybackHistory(0, page * EPISODES_PER_PAGE, startDate, endDate, sortOrder).toMutableList() +// FeedItemPermutors.getPermutor(sortOrder).reorder(hList) + return hList } override fun loadMoreData(page: Int): List { - return DBReader.getPlaybackHistory((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE) + val hList = DBReader.getPlaybackHistory((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, startDate, endDate, sortOrder).toMutableList() +// FeedItemPermutors.getPermutor(sortOrder).reorder(hList) + return hList } override fun loadTotalItemCount(): Int { return DBReader.getPlaybackHistoryLength().toInt() } + class HistorySortDialog : ItemSortDialog() { + override fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) { + if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.PLAYED_DATE_OLD_NEW + || ascending == SortOrder.COMPLETED_DATE_OLD_NEW + || ascending == SortOrder.DURATION_SHORT_LONG || ascending == SortOrder.EPISODE_TITLE_A_Z + || ascending == SortOrder.SIZE_SMALL_LARGE || ascending == SortOrder.FEED_TITLE_A_Z) { + super.onAddItem(title, ascending, descending, ascendingIsDefault) + } + } + override fun onSelectionChanged() { + super.onSelectionChanged() + EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder?: SortOrder.PLAYED_DATE_NEW_OLD)) + } + } + companion object { const val TAG: String = "PlaybackHistoryFragment" } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt index a7f69073..478bd6e8 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt @@ -18,7 +18,8 @@ import ac.mdiq.podcini.util.ChapterUtils import ac.mdiq.podcini.util.DateFormatter import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.NetworkUtils.fetchHtmlSource -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet @@ -41,17 +42,20 @@ import androidx.core.content.ContextCompat import androidx.core.graphics.BlendModeColorFilterCompat import androidx.core.graphics.BlendModeCompat import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import coil.imageLoader import coil.request.ErrorResult import coil.request.ImageRequest import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import net.dankito.readability4j.Readability4J import org.apache.commons.lang3.StringUtils -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode /** * Displays the description of a Playable object in a Webview. @@ -68,7 +72,7 @@ class PlayerDetailsFragment : Fragment() { private var item: FeedItem? = null private var displayedChapterIndex = -1 - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) private var cleanedNotes: String? = null // private var webViewLoader: Disposable? = null private var controller: PlaybackController? = null @@ -114,6 +118,11 @@ class PlayerDetailsFragment : Fragment() { return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun onDestroyView() { super.onDestroyView() _binding = null @@ -172,7 +181,7 @@ class PlayerDetailsFragment : Fragment() { private fun load() { val context = context ?: return - scope.launch { + lifecycleScope.launch { withContext(Dispatchers.IO) { if (item == null) { media = controller?.getMedia() @@ -443,8 +452,18 @@ class PlayerDetailsFragment : Fragment() { savePreference() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event) + else -> {} + } + } + } + } + + fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) { val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(media, event.position) if (newChapterIndex > -1 && newChapterIndex != displayedChapterIndex) { refreshChapterData(newChapterIndex) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/QueueFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/QueueFragment.kt index 0711d08f..48c05ead 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/QueueFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/QueueFragment.kt @@ -28,8 +28,8 @@ import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder import ac.mdiq.podcini.util.Converter import ac.mdiq.podcini.util.FeedItemUtil import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.* -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences @@ -41,6 +41,7 @@ import android.widget.ProgressBar import android.widget.TextView import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView @@ -51,16 +52,16 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialView -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.* /** * Shows all items in the queue. */ -class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAdapter.OnSelectModeListener { +@UnstableApi class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAdapter.OnSelectModeListener { private var _binding: QueueFragmentBinding? = null private val binding get() = _binding!! @@ -81,7 +82,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda private var recyclerAdapter: QueueRecyclerAdapter? = null private var currentPlaying: EpisodeItemViewHolder? = null - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) // private var disposable: Disposable? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -175,14 +176,14 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda recyclerAdapter?.endSelectMode() true } - loadItems(true) - EventBus.getDefault().register(this) return binding.root } override fun onStart() { super.onStart() + loadItems(true) + procFlowEvents() if (queue.isNotEmpty()) recyclerView.restoreScrollPosition(TAG) } @@ -196,36 +197,65 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda //// disposable?.dispose() // } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: QueueEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.QueueEvent -> onEventMainThread(event) + is FlowEvent.FeedItemEvent -> onEventMainThread(event) + is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event) + is FlowEvent.PlayerStatusEvent -> onPlayerStatusChanged(event) + is FlowEvent.UnreadItemsUpdateEvent -> onUnreadItemsChanged(event) + is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.EpisodeDownloadEvent -> onEventMainThread(event) + is FlowEvent.FeedUpdateRunningEvent -> swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.keyEvents.collectLatest { event -> + onKeyUp(event) + } + } + } + + fun onEventMainThread(event: FlowEvent.QueueEvent) { Logd(TAG, "onEventMainThread() called with QueueEvent event = [$event]") if (recyclerAdapter == null) { loadItems(true) return } when (event.action) { - QueueEvent.Action.ADDED -> { + FlowEvent.QueueEvent.Action.ADDED -> { if (event.item != null) queue.add(event.position, event.item) recyclerAdapter?.notifyItemInserted(event.position) } - QueueEvent.Action.SET_QUEUE, QueueEvent.Action.SORTED -> { + FlowEvent.QueueEvent.Action.SET_QUEUE, FlowEvent.QueueEvent.Action.SORTED -> { queue = event.items.toMutableList() recyclerAdapter?.updateItems(event.items) } - QueueEvent.Action.REMOVED, QueueEvent.Action.IRREVERSIBLE_REMOVED -> { + FlowEvent.QueueEvent.Action.REMOVED, FlowEvent.QueueEvent.Action.IRREVERSIBLE_REMOVED -> { if (event.item != null) { val position: Int = FeedItemUtil.indexOfItemWithId(queue.toList(), event.item.id) queue.removeAt(position) recyclerAdapter?.notifyItemRemoved(position) } } - QueueEvent.Action.CLEARED -> { + FlowEvent.QueueEvent.Action.CLEARED -> { queue.clear() recyclerAdapter?.updateItems(queue) } - QueueEvent.Action.MOVED -> return - QueueEvent.Action.ADDED_ITEMS -> return - QueueEvent.Action.DELETED_MEDIA -> return + FlowEvent.QueueEvent.Action.MOVED -> return + FlowEvent.QueueEvent.Action.ADDED_ITEMS -> return + FlowEvent.QueueEvent.Action.DELETED_MEDIA -> return } recyclerAdapter?.updateDragDropEnabled() refreshToolbarState() @@ -233,8 +263,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda refreshInfoBar() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedItemEvent) { + fun onEventMainThread(event: FlowEvent.FeedItemEvent) { Logd(TAG, "onEventMainThread() called with FeedItemEvent event = [$event]") if (recyclerAdapter == null) { loadItems(true) @@ -255,8 +284,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda } } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: EpisodeDownloadEvent) { + fun onEventMainThread(event: FlowEvent.EpisodeDownloadEvent) { Logd(TAG, "onEventMainThread() called with EpisodeDownloadEvent event = [$event]") for (downloadUrl in event.urls) { val pos: Int = FeedItemUtil.indexOfItemWithDownloadUrl(queue.toList(), downloadUrl) @@ -264,8 +292,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent) { + fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) { // Log.d(TAG, "onEventMainThread() called with PlaybackPositionEvent event = [$event]") if (recyclerAdapter != null) { if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem) currentPlaying!!.notifyPlaybackPositionUpdated(event) @@ -283,15 +310,13 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlayerStatusChanged(event: PlayerStatusEvent?) { + fun onPlayerStatusChanged(event: FlowEvent.PlayerStatusEvent?) { Logd(TAG, "onPlayerStatusChanged() called with event = [$event]") loadItems(false) refreshToolbarState() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { + fun onUnreadItemsChanged(event: FlowEvent.UnreadItemsUpdateEvent?) { // Sent when playback position is reset Logd(TAG, "onUnreadItemsChanged() called with event = [$event]") loadItems(false) @@ -310,17 +335,11 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda //// } // } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) { - refreshSwipeTelltale() - } - private fun refreshSwipeTelltale() { if (swipeActions.actions?.left != null) binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon()) if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon()) } - @Subscribe(threadMode = ThreadMode.MAIN) fun onKeyUp(event: KeyEvent) { if (!isAdded || !isVisible || !isMenuVisible) return @@ -336,8 +355,8 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda _binding = null recyclerAdapter?.endSelectMode() recyclerAdapter = null - EventBus.getDefault().unregister(this) - scope.cancel() + +// scope.cancel() toolbar.setOnMenuItemClickListener(null) toolbar.setOnLongClickListener(null) @@ -349,11 +368,6 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted) } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedUpdateRunningEvent) { - swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning - } - @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { val itemId = item.itemId when (itemId) { @@ -502,7 +516,7 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda // refreshInfoBar() // }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - scope.launch { + lifecycleScope.launch { try { queue = withContext(Dispatchers.IO) { DBReader.getQueue().toMutableList() } withContext(Dispatchers.Main) { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/QuickFeedDiscoveryFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/QuickFeedDiscoveryFragment.kt index ad6a9931..528f997f 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/QuickFeedDiscoveryFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/QuickFeedDiscoveryFragment.kt @@ -9,7 +9,8 @@ import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.adapter.FeedDiscoverAdapter import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.DiscoveryDefaultUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.content.SharedPreferences import android.os.Bundle @@ -21,11 +22,12 @@ import android.view.ViewGroup import android.widget.* import androidx.annotation.OptIn import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.* class QuickFeedDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener { @@ -33,7 +35,7 @@ class QuickFeedDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener { private val binding get() = _binding!! // private var disposable: Disposable? = null - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) private lateinit var adapter: FeedDiscoverAdapter private lateinit var discoverGridLayout: GridView @@ -75,22 +77,31 @@ class QuickFeedDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener { adapter.updateData(dummies) loadToplist() - EventBus.getDefault().register(this) return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun onDestroy() { super.onDestroy() _binding = null - EventBus.getDefault().unregister(this) - scope.cancel() + +// scope.cancel() // disposable?.dispose() } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun onDiscoveryDefaultUpdateEvent(event: DiscoveryDefaultUpdateEvent?) { - loadToplist() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.DiscoveryDefaultUpdateEvent -> loadToplist() + else -> {} + } + } + } } private fun loadToplist() { @@ -149,7 +160,7 @@ class QuickFeedDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener { // errorRetry.setOnClickListener { loadToplist() } // }) - scope.launch { + lifecycleScope.launch { try { val podcasts = withContext(Dispatchers.IO) { loader.loadToplist(countryCode, NUM_SUGGESTIONS, DBReader.getFeedList()) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index b3f11149..1ac3f8df 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -22,8 +22,8 @@ import ac.mdiq.podcini.ui.view.LiftOnScrollListener import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder import ac.mdiq.podcini.util.FeedItemUtil import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.* -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.os.Bundle import android.os.Handler @@ -35,6 +35,7 @@ import android.view.inputmethod.InputMethodManager import android.widget.ProgressBar import androidx.appcompat.widget.SearchView import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -43,15 +44,15 @@ import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialView -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Performs a search operation on all feeds or one specific feed and displays the search result. */ -class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { +@UnstableApi class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { private var _binding: SearchFragmentBinding? = null private val binding get() = _binding!! @@ -68,7 +69,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { private var results: MutableList = mutableListOf() private var currentPlaying: EpisodeItemViewHolder? = null - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) // private var disposable: Disposable? = null private var lastQueryChange: Long = 0 private var isOtherViewInFoucus = false @@ -81,7 +82,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { override fun onStop() { super.onStop() - scope.cancel() +// scope.cancel() // disposable?.dispose() } @@ -124,7 +125,6 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { emptyViewHandler.setIcon(R.drawable.ic_search) emptyViewHandler.setTitle(R.string.search_status_no_results) emptyViewHandler.setMessage(R.string.type_to_search) - EventBus.getDefault().register(this) chip = binding.feedTitleChip chip.setOnCloseIconClickListener { @@ -171,10 +171,15 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun onDestroyView() { super.onDestroyView() _binding = null - EventBus.getDefault().unregister(this) + } private fun setupToolbar(toolbar: MaterialToolbar) { @@ -231,18 +236,28 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { return super.onContextItemSelected(item) } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onFeedListChanged(event: FeedListUpdateEvent?) { - search() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.FeedListUpdateEvent, is FlowEvent.UnreadItemsUpdateEvent, is FlowEvent.PlayerStatusEvent -> search() + is FlowEvent.FeedItemEvent -> onEventMainThread(event) + is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event) + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.EpisodeDownloadEvent -> onEventMainThread(event) + else -> {} + } + } + } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { - search() - } - - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedItemEvent) { + fun onEventMainThread(event: FlowEvent.FeedItemEvent) { Logd(TAG, "onEventMainThread() called with: event = [$event]") var i = 0 @@ -259,16 +274,14 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { } } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: EpisodeDownloadEvent) { + fun onEventMainThread(event: FlowEvent.EpisodeDownloadEvent) { for (downloadUrl in event.urls) { val pos: Int = FeedItemUtil.indexOfItemWithDownloadUrl(results, downloadUrl) if (pos >= 0) adapter.notifyItemChangedCompat(pos) } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent) { + fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) { if (currentPlaying != null && currentPlaying!!.isCurrentlyPlayingItem) currentPlaying!!.notifyPlaybackPositionUpdated(event) else { @@ -284,11 +297,6 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { } } - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlayerStatusChanged(event: PlayerStatusEvent?) { - search() - } - @UnstableApi private fun searchWithProgressBar() { progressBar.visibility = View.VISIBLE emptyViewHandler.hide() @@ -318,7 +326,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener { // // }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) - scope.launch { + lifecycleScope.launch { try { val results = withContext(Dispatchers.IO) { performSearch() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/SubscriptionFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/SubscriptionFragment.kt index 87022476..7932a2ea 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/SubscriptionFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/SubscriptionFragment.kt @@ -19,10 +19,8 @@ import ac.mdiq.podcini.ui.dialog.SubscriptionsFilterDialog import ac.mdiq.podcini.ui.view.EmptyViewHandler import ac.mdiq.podcini.ui.view.LiftOnScrollListener import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.FeedListUpdateEvent -import ac.mdiq.podcini.util.event.FeedTagsChangedEvent -import ac.mdiq.podcini.util.event.FeedUpdateRunningEvent -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.content.SharedPreferences import android.os.Bundle @@ -32,6 +30,7 @@ import android.view.inputmethod.EditorInfo import android.widget.* import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -40,10 +39,10 @@ import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.floatingactionbutton.FloatingActionButton import com.leinardi.android.speeddial.SpeedDialActionItem import com.leinardi.android.speeddial.SpeedDialView -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.* /** @@ -70,7 +69,7 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select private var displayedFolder: String = "" private var displayUpArrow = false - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) // private var disposable: Disposable? = null private var feedList: List = mutableListOf() private var feedListFiltered: List = mutableListOf() @@ -190,17 +189,21 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select true } - EventBus.getDefault().register(this) loadSubscriptions() return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun onDestroyView() { super.onDestroyView() _binding = null - EventBus.getDefault().unregister(this) - scope.cancel() + +// scope.cancel() // disposable?.dispose() } @@ -230,9 +233,25 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select subscriptionAdapter.setItems(feedListFiltered) } - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: FeedUpdateRunningEvent) { - swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event) + is FlowEvent.UnreadItemsUpdateEvent -> loadSubscriptions() + is FlowEvent.FeedTagsChangedEvent -> resetTags() + else -> {} + } + } + } + lifecycleScope.launch { + EventFlow.stickyEvents.collectLatest { event -> + when (event) { + is FlowEvent.FeedUpdateRunningEvent -> swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning + else -> {} + } + } + } } @UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean { @@ -295,7 +314,7 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select // Log.e(TAG, Log.getStackTraceString(error)) // }) - scope.launch { + lifecycleScope.launch { try { val result = withContext(Dispatchers.IO) { val data: NavDrawerData = DBReader.getNavDrawerData(UserPreferences.subscriptionsFilter) @@ -333,22 +352,11 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select return FeedMenuHandler.onMenuItemClicked(this, item.itemId, feed) { this.loadSubscriptions() } } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onFeedListChanged(event: FeedListUpdateEvent?) { + fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent?) { DBReader.updateFeedList() loadSubscriptions() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsChanged(event: UnreadItemsUpdateEvent?) { - loadSubscriptions() - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onFeedTagsChanged(event: FeedTagsChangedEvent?) { - resetTags() - } - override fun onEndSelectMode() { speedDialView.close() speedDialView.visibility = View.GONE diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt index 6cff0fbc..fe48884c 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt @@ -25,8 +25,8 @@ import ac.mdiq.podcini.ui.view.ShownotesWebView import ac.mdiq.podcini.util.Converter.getDurationStringLong import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.TimeSpeedConverter -import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle import android.os.Handler import android.os.Looper @@ -41,12 +41,12 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat.invalidateOptionsMenu import androidx.fragment.app.Fragment import androidx.interpolator.view.animation.FastOutSlowInInterpolator +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.lang.Runnable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @UnstableApi class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { @@ -63,7 +63,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { private val videoControlsHider = Handler(Looper.getMainLooper()) private var showTimeLeft = false - val scope = CoroutineScope(Dispatchers.Main) +// val scope = CoroutineScope(Dispatchers.Main) // private var disposable: Disposable? = null private var prog = 0f @@ -95,9 +95,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { override fun updatePlayButtonShowsPlay(showPlay: Boolean) { Logd(TAG, "updatePlayButtonShowsPlay called") binding.playButton.setIsShowPlay(showPlay) - if (showPlay) { - (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { + if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + else { (activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) setupVideoAspectRatio() if (videoSurfaceCreated && controller != null) { @@ -121,7 +120,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { override fun onStart() { super.onStart() onPositionObserverUpdate() - EventBus.getDefault().register(this) + procFlowEvents() } @UnstableApi @@ -134,7 +133,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { @UnstableApi override fun onStop() { - EventBus.getDefault().unregister(this) + super.onStop() if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls) @@ -149,13 +148,23 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { _binding = null controller?.release() controller = null // prevent leak - scope.cancel() +// scope.cancel() // disposable?.dispose() } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun bufferUpdate(event: BufferUpdateEvent) { + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.BufferUpdateEvent -> bufferUpdate(event) + is FlowEvent.PlaybackPositionEvent -> onPositionObserverUpdate() + else -> {} + } + } + } + } + + fun bufferUpdate(event: FlowEvent.BufferUpdateEvent) { when { event.hasStarted() -> binding.progressBar.visibility = View.VISIBLE event.hasEnded() -> binding.progressBar.visibility = View.INVISIBLE @@ -227,7 +236,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { // Log.e(TAG, Log.getStackTraceString(error)) // }) - scope.launch { + lifecycleScope.launch { try { item = withContext(Dispatchers.IO) { loadInBackground() @@ -504,11 +513,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { binding.controlsContainer.visibility = View.GONE } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: PlaybackPositionEvent?) { - onPositionObserverUpdate() - } - fun onPositionObserverUpdate() { if (controller == null) return diff --git a/app/src/main/java/ac/mdiq/podcini/ui/statistics/StatisticsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/statistics/StatisticsFragment.kt index 6134a2aa..41f2fb43 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/statistics/StatisticsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/statistics/StatisticsFragment.kt @@ -5,12 +5,13 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.PagerFragmentBinding import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.ui.activity.MainActivity -import ac.mdiq.podcini.ui.fragment.PagedToolbarFragment import ac.mdiq.podcini.ui.dialog.ConfirmationDialog +import ac.mdiq.podcini.ui.fragment.PagedToolbarFragment import ac.mdiq.podcini.ui.statistics.downloads.DownloadStatisticsFragment import ac.mdiq.podcini.ui.statistics.subscriptions.SubscriptionStatisticsFragment import ac.mdiq.podcini.ui.statistics.years.YearsStatisticsFragment -import ac.mdiq.podcini.util.event.StatisticsEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.content.DialogInterface import android.os.Bundle @@ -21,16 +22,16 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.OptIn import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator -import io.reactivex.Completable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Displays the 'statistics' screen @@ -103,11 +104,24 @@ class StatisticsFragment : PagedToolbarFragment() { .putLong(PREF_FILTER_TO, Long.MAX_VALUE) .apply() - val disposable = Completable.fromFuture(DBWriter.resetStatistics()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ EventBus.getDefault().post(StatisticsEvent()) }, - { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) +// val disposable = Completable.fromFuture(DBWriter.resetStatistics()) +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ EventFlow.postEvent(FlowEvent.StatisticsEvent()) }, +// { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + + lifecycleScope.launch { + try { + withContext(Dispatchers.IO) { + DBWriter.resetStatistics() + } + // This runs on the Main thread + EventFlow.postEvent(FlowEvent.StatisticsEvent()) + } catch (error: Throwable) { + // This also runs on the Main thread + Log.e(TAG, Log.getStackTraceString(error)) + } + } } class StatisticsPagerAdapter internal constructor(fragment: Fragment) : FragmentStateAdapter(fragment) { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/statistics/downloads/DownloadStatisticsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/statistics/downloads/DownloadStatisticsFragment.kt index 2a0acf51..5713f08d 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/statistics/downloads/DownloadStatisticsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/statistics/downloads/DownloadStatisticsFragment.kt @@ -14,12 +14,16 @@ import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Displays the 'download statistics' screen @@ -29,16 +33,14 @@ class DownloadStatisticsFragment : Fragment() { private var _binding: StatisticsFragmentBinding? = null private val binding get() = _binding!! - private var disposable: Disposable? = null +// private var disposable: Disposable? = null private lateinit var downloadStatisticsList: RecyclerView private lateinit var progressBar: ProgressBar private lateinit var listAdapter: DownloadStatisticsListAdapter - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = StatisticsFragmentBinding.inflate(inflater) downloadStatisticsList = binding.statisticsList progressBar = binding.progressBar @@ -68,24 +70,41 @@ class DownloadStatisticsFragment : Fragment() { } private fun loadStatistics() { - disposable?.dispose() +// disposable?.dispose() - disposable = - Observable.fromCallable { - // Filters do not matter here - val statisticsData = DBReader.getStatistics(false, 0, Long.MAX_VALUE) - statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> - item2.totalDownloadSize.compareTo(item1.totalDownloadSize) +// disposable = Observable.fromCallable { +// // Filters do not matter here +// val statisticsData = DBReader.getStatistics(false, 0, Long.MAX_VALUE) +// statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> +// item2.totalDownloadSize.compareTo(item1.totalDownloadSize) +// } +// statisticsData +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ result: StatisticsResult -> +// listAdapter.update(result.feedTime) +// progressBar.visibility = View.GONE +// downloadStatisticsList.visibility = View.VISIBLE +// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + + lifecycleScope.launch { + try { + val statisticsData = withContext(Dispatchers.IO) { + val data = DBReader.getStatistics(false, 0, Long.MAX_VALUE) + data.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> + item2.totalDownloadSize.compareTo(item1.totalDownloadSize) + } + data } - statisticsData + listAdapter.update(statisticsData.feedTime) + progressBar.visibility = View.GONE + downloadStatisticsList.visibility = View.VISIBLE + } catch (error: Throwable) { + Log.e(TAG, Log.getStackTraceString(error)) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ result: StatisticsResult -> - listAdapter.update(result.feedTime) - progressBar.visibility = View.GONE - downloadStatisticsList.visibility = View.VISIBLE - }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + } + } companion object { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/statistics/feed/FeedStatisticsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/statistics/feed/FeedStatisticsFragment.kt index 702f6269..f35de616 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/statistics/feed/FeedStatisticsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/statistics/feed/FeedStatisticsFragment.kt @@ -1,19 +1,20 @@ package ac.mdiq.podcini.ui.statistics.feed import ac.mdiq.podcini.databinding.FeedStatisticsBinding +import ac.mdiq.podcini.storage.DBReader +import ac.mdiq.podcini.storage.StatisticsItem +import ac.mdiq.podcini.util.Converter.shortLocalizedDuration import android.os.Bundle import android.text.format.Formatter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment -import ac.mdiq.podcini.storage.DBReader -import ac.mdiq.podcini.storage.StatisticsItem -import ac.mdiq.podcini.util.Converter.shortLocalizedDuration -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers +import androidx.lifecycle.lifecycleScope import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.* class FeedStatisticsFragment : Fragment() { @@ -21,11 +22,9 @@ class FeedStatisticsFragment : Fragment() { private val binding get() = _binding!! private var feedId: Long = 0 - private var disposable: Disposable? = null +// private var disposable: Disposable? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { feedId = requireArguments().getLong(EXTRA_FEED_ID) _binding = FeedStatisticsBinding.inflate(inflater) @@ -43,33 +42,47 @@ class FeedStatisticsFragment : Fragment() { } private fun loadStatistics() { - disposable = - Observable.fromCallable { - val statisticsData = DBReader.getStatistics(true, 0, Long.MAX_VALUE) - statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> - java.lang.Long.compare(item2.timePlayed, - item1.timePlayed) - } +// disposable = Observable.fromCallable { +// val statisticsData = DBReader.getStatistics(true, 0, Long.MAX_VALUE) +// statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> +// java.lang.Long.compare(item2.timePlayed, +// item1.timePlayed) +// } +// +// for (statisticsItem in statisticsData.feedTime) { +// if (statisticsItem.feed.id == feedId) { +// return@fromCallable statisticsItem +// } +// } +// null +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ s: StatisticsItem? -> this.showStats(s) }, { obj: Throwable -> obj.printStackTrace() }) - for (statisticsItem in statisticsData.feedTime) { - if (statisticsItem.feed.id == feedId) { - return@fromCallable statisticsItem + lifecycleScope.launch { + try { + val statisticsData = withContext(Dispatchers.IO) { + val data = DBReader.getStatistics(true, 0, Long.MAX_VALUE) + data.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> + item2.timePlayed.compareTo(item1.timePlayed) } + for (statisticsItem in data.feedTime) { + if (statisticsItem.feed.id == feedId) return@withContext statisticsItem + } + null } - null + showStats(statisticsData) + } catch (error: Throwable) { + error.printStackTrace() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ s: StatisticsItem? -> this.showStats(s) }, { obj: Throwable -> obj.printStackTrace() }) + } } private fun showStats(s: StatisticsItem?) { - binding.startedTotalLabel.text = String.format(Locale.getDefault(), "%d / %d", - s!!.episodesStarted, s.episodes) - binding.timePlayedLabel.text = - shortLocalizedDuration(requireContext(), s.timePlayed) - binding.totalDurationLabel.text = - shortLocalizedDuration(requireContext(), s.time) + binding.startedTotalLabel.text = String.format(Locale.getDefault(), "%d / %d", s!!.episodesStarted, s.episodes) + binding.timePlayedLabel.text = shortLocalizedDuration(requireContext(), s.timePlayed) + binding.totalDurationLabel.text = shortLocalizedDuration(requireContext(), s.time) binding.onDeviceLabel.text = String.format(Locale.getDefault(), "%d", s.episodesDownloadCount) binding.spaceUsedLabel.text = Formatter.formatShortFileSize(context, s.totalDownloadSize) } @@ -77,7 +90,7 @@ class FeedStatisticsFragment : Fragment() { override fun onDestroy() { super.onDestroy() _binding = null - disposable?.dispose() +// disposable?.dispose() } companion object { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/DatesFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/DatesFilterDialog.kt new file mode 100644 index 00000000..83ca93e2 --- /dev/null +++ b/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/DatesFilterDialog.kt @@ -0,0 +1,134 @@ +package ac.mdiq.podcini.ui.statistics.subscriptions + + +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.StatisticsFilterDialogBinding +import android.content.Context +import android.content.DialogInterface +import android.content.SharedPreferences +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.View +import android.widget.ArrayAdapter +import android.widget.CompoundButton +import androidx.core.util.Pair +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.text.SimpleDateFormat +import java.util.* +import kotlin.math.max + +abstract class DatesFilterDialog(private val context: Context, oldestDate: Long) { + + protected var prefs: SharedPreferences? = null + protected var includeMarkedAsPlayed: Boolean = false + protected var timeFilterFrom: Long = 0L + protected var timeFilterTo: Long = Date().time + protected var showMarkPlayed = true + + protected val filterDatesFrom: Pair, Array> + protected val filterDatesTo: Pair, Array> + + + init { + initParams() + filterDatesFrom = makeMonthlyList(oldestDate, false) + filterDatesTo = makeMonthlyList(oldestDate, true) + } + +// set prefs, includeMarkedAsPlayed, timeFilterFrom, timeFilterTo + abstract fun initParams() + abstract fun callback(timeFilterFrom: Long, timeFilterTo: Long, includeMarkedAsPlayed: Boolean = true) + + fun show() { + val binding = StatisticsFilterDialogBinding.inflate(LayoutInflater.from(context)) + val builder = MaterialAlertDialogBuilder(context) + builder.setView(binding.root) + builder.setTitle(R.string.filter) + binding.includeMarkedCheckbox.setOnCheckedChangeListener { compoundButton: CompoundButton?, checked: Boolean -> + binding.timeToSpinner.isEnabled = !checked + binding.timeFromSpinner.isEnabled = !checked + binding.pastYearButton.isEnabled = !checked + binding.allTimeButton.isEnabled = !checked + binding.dateSelectionContainer.alpha = if (checked) 0.5f else 1f + } + if (showMarkPlayed) { + binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed + } else { + binding.includeMarkedCheckbox.visibility = View.GONE + binding.noticeMessage.visibility = View.GONE + } + + val adapterFrom = ArrayAdapter(context, android.R.layout.simple_spinner_item, filterDatesFrom.first) + adapterFrom.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.timeFromSpinner.adapter = adapterFrom + for (i in filterDatesFrom.second.indices) { + if (filterDatesFrom.second[i] >= timeFilterFrom) { + binding.timeFromSpinner.setSelection(i) + break + } + } + + val adapterTo = ArrayAdapter(context, android.R.layout.simple_spinner_item, filterDatesTo.first) + adapterTo.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding.timeToSpinner.adapter = adapterTo + for (i in filterDatesTo.second.indices) { + if (filterDatesTo.second[i] >= timeFilterTo) { + binding.timeToSpinner.setSelection(i) + break + } + } + + binding.allTimeButton.setOnClickListener { v: View? -> + binding.timeFromSpinner.setSelection(0) + binding.timeToSpinner.setSelection(filterDatesTo.first.size - 1) + } + binding.pastYearButton.setOnClickListener { v: View? -> + binding.timeFromSpinner.setSelection(max(0.0, (filterDatesFrom.first.size - 12).toDouble()).toInt()) + binding.timeToSpinner.setSelection(filterDatesTo.first.size - 2) + } + + builder.setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> + includeMarkedAsPlayed = binding.includeMarkedCheckbox.isChecked + if (includeMarkedAsPlayed) { + // We do not know the date at which something was marked as played, so filtering does not make sense + timeFilterFrom = 0 + timeFilterTo = Long.MAX_VALUE + } else { + timeFilterFrom = filterDatesFrom.second[binding.timeFromSpinner.selectedItemPosition] + timeFilterTo = filterDatesTo.second[binding.timeToSpinner.selectedItemPosition] + } + callback(timeFilterFrom, timeFilterTo, includeMarkedAsPlayed) + } + builder.show() + } + + private fun makeMonthlyList(oldestDate: Long, inclusive: Boolean): Pair, Array> { + val date = Calendar.getInstance() + date.timeInMillis = oldestDate + date[Calendar.HOUR_OF_DAY] = 0 + date[Calendar.MINUTE] = 0 + date[Calendar.SECOND] = 0 + date[Calendar.MILLISECOND] = 0 + date[Calendar.DAY_OF_MONTH] = 1 + val names = ArrayList() + val timestamps = ArrayList() + val skeleton = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMM yyyy") + val dateFormat = SimpleDateFormat(skeleton, Locale.getDefault()) + while (date.timeInMillis < System.currentTimeMillis()) { + names.add(dateFormat.format(Date(date.timeInMillis))) + if (!inclusive) timestamps.add(date.timeInMillis) + + if (date[Calendar.MONTH] == Calendar.DECEMBER) { + date[Calendar.MONTH] = Calendar.JANUARY + date[Calendar.YEAR] = date[Calendar.YEAR] + 1 + } else date[Calendar.MONTH] = date[Calendar.MONTH] + 1 + + if (inclusive) timestamps.add(date.timeInMillis) + } + if (inclusive) { + names.add(context.getString(R.string.statistics_today)) + timestamps.add(Long.MAX_VALUE) + } + return Pair(names.toTypedArray(), timestamps.toTypedArray()) + } +} diff --git a/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/StatisticsFilterDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/StatisticsFilterDialog.kt deleted file mode 100644 index 1b202997..00000000 --- a/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/StatisticsFilterDialog.kt +++ /dev/null @@ -1,141 +0,0 @@ -package ac.mdiq.podcini.ui.statistics.subscriptions - - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.StatisticsFilterDialogBinding -import ac.mdiq.podcini.ui.statistics.StatisticsFragment -import ac.mdiq.podcini.util.event.StatisticsEvent -import android.content.Context -import android.content.DialogInterface -import android.content.SharedPreferences -import android.text.format.DateFormat -import android.view.LayoutInflater -import android.view.View -import android.widget.ArrayAdapter -import android.widget.CompoundButton -import androidx.core.util.Pair -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.greenrobot.eventbus.EventBus -import java.text.SimpleDateFormat -import java.util.* -import kotlin.math.max - -class StatisticsFilterDialog(private val context: Context, oldestDate: Long) { - private val prefs: SharedPreferences = - context.getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE) - private var includeMarkedAsPlayed: Boolean - private var timeFilterFrom: Long - private var timeFilterTo: Long - private val filterDatesFrom: Pair, Array> - private val filterDatesTo: Pair, Array> - - init { - includeMarkedAsPlayed = prefs.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false) - timeFilterFrom = prefs.getLong(StatisticsFragment.PREF_FILTER_FROM, 0) - timeFilterTo = prefs.getLong(StatisticsFragment.PREF_FILTER_TO, Long.MAX_VALUE) - filterDatesFrom = makeMonthlyList(oldestDate, false) - filterDatesTo = makeMonthlyList(oldestDate, true) - } - - fun show() { - val dialogBinding = StatisticsFilterDialogBinding.inflate( - LayoutInflater.from(context)) - val builder = MaterialAlertDialogBuilder(context) - builder.setView(dialogBinding.root) - builder.setTitle(R.string.filter) - dialogBinding.includeMarkedCheckbox.setOnCheckedChangeListener { compoundButton: CompoundButton?, checked: Boolean -> - dialogBinding.timeToSpinner.isEnabled = !checked - dialogBinding.timeFromSpinner.isEnabled = !checked - dialogBinding.pastYearButton.isEnabled = !checked - dialogBinding.allTimeButton.isEnabled = !checked - dialogBinding.dateSelectionContainer.alpha = if (checked) 0.5f else 1f - } - dialogBinding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed - - - val adapterFrom = ArrayAdapter(context, - android.R.layout.simple_spinner_item, filterDatesFrom.first) - adapterFrom.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - dialogBinding.timeFromSpinner.adapter = adapterFrom - for (i in filterDatesFrom.second.indices) { - if (filterDatesFrom.second[i] >= timeFilterFrom) { - dialogBinding.timeFromSpinner.setSelection(i) - break - } - } - - val adapterTo = ArrayAdapter(context, - android.R.layout.simple_spinner_item, filterDatesTo.first) - adapterTo.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - dialogBinding.timeToSpinner.adapter = adapterTo - for (i in filterDatesTo.second.indices) { - if (filterDatesTo.second[i] >= timeFilterTo) { - dialogBinding.timeToSpinner.setSelection(i) - break - } - } - - dialogBinding.allTimeButton.setOnClickListener { v: View? -> - dialogBinding.timeFromSpinner.setSelection(0) - dialogBinding.timeToSpinner.setSelection(filterDatesTo.first.size - 1) - } - dialogBinding.pastYearButton.setOnClickListener { v: View? -> - dialogBinding.timeFromSpinner.setSelection(max(0.0, (filterDatesFrom.first.size - 12).toDouble()) - .toInt()) - dialogBinding.timeToSpinner.setSelection(filterDatesTo.first.size - 2) - } - - builder.setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> - includeMarkedAsPlayed = dialogBinding.includeMarkedCheckbox.isChecked - if (includeMarkedAsPlayed) { - // We do not know the date at which something was marked as played, so filtering does not make sense - timeFilterFrom = 0 - timeFilterTo = Long.MAX_VALUE - } else { - timeFilterFrom = filterDatesFrom.second[dialogBinding.timeFromSpinner.selectedItemPosition] - timeFilterTo = filterDatesTo.second[dialogBinding.timeToSpinner.selectedItemPosition] - } - prefs.edit() - .putBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed) - .putLong(StatisticsFragment.PREF_FILTER_FROM, timeFilterFrom) - .putLong(StatisticsFragment.PREF_FILTER_TO, timeFilterTo) - .apply() - EventBus.getDefault().post(StatisticsEvent()) - } - builder.show() - } - - private fun makeMonthlyList(oldestDate: Long, inclusive: Boolean): Pair, Array> { - val date = Calendar.getInstance() - date.timeInMillis = oldestDate - date[Calendar.HOUR_OF_DAY] = 0 - date[Calendar.MINUTE] = 0 - date[Calendar.SECOND] = 0 - date[Calendar.MILLISECOND] = 0 - date[Calendar.DAY_OF_MONTH] = 1 - val names = ArrayList() - val timestamps = ArrayList() - val skeleton = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMM yyyy") - val dateFormat = SimpleDateFormat(skeleton, Locale.getDefault()) - while (date.timeInMillis < System.currentTimeMillis()) { - names.add(dateFormat.format(Date(date.timeInMillis))) - if (!inclusive) { - timestamps.add(date.timeInMillis) - } - if (date[Calendar.MONTH] == Calendar.DECEMBER) { - date[Calendar.MONTH] = Calendar.JANUARY - date[Calendar.YEAR] = date[Calendar.YEAR] + 1 - } else { - date[Calendar.MONTH] = date[Calendar.MONTH] + 1 - } - if (inclusive) { - timestamps.add(date.timeInMillis) - } - } - if (inclusive) { - names.add(context.getString(R.string.statistics_today)) - timestamps.add(Long.MAX_VALUE) - } - return Pair(names.toTypedArray(), timestamps.toTypedArray()) - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/SubscriptionStatisticsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/SubscriptionStatisticsFragment.kt index f0072f7a..3fbbad10 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/SubscriptionStatisticsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/statistics/subscriptions/SubscriptionStatisticsFragment.kt @@ -4,25 +4,31 @@ package ac.mdiq.podcini.ui.statistics.subscriptions import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.StatisticsFragmentBinding import ac.mdiq.podcini.storage.DBReader +import ac.mdiq.podcini.storage.DBReader.MonthlyStatisticsItem import ac.mdiq.podcini.storage.DBReader.StatisticsResult import ac.mdiq.podcini.storage.StatisticsItem import ac.mdiq.podcini.ui.statistics.StatisticsFragment -import ac.mdiq.podcini.util.event.StatisticsEvent +import ac.mdiq.podcini.ui.statistics.years.YearsStatisticsFragment +import ac.mdiq.podcini.ui.statistics.years.YearsStatisticsFragment.Companion +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.content.Context import android.os.Bundle import android.util.Log import android.view.* import android.widget.ProgressBar import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlin.math.max import kotlin.math.min @@ -33,7 +39,7 @@ class SubscriptionStatisticsFragment : Fragment() { private var _binding: StatisticsFragmentBinding? = null private val binding get() = _binding!! - private var disposable: Disposable? = null +// private var disposable: Disposable? = null private var statisticsResult: StatisticsResult? = null private lateinit var feedStatisticsList: RecyclerView @@ -45,31 +51,38 @@ class SubscriptionStatisticsFragment : Fragment() { setHasOptionsMenu(true) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = StatisticsFragmentBinding.inflate(inflater) feedStatisticsList = binding.statisticsList progressBar = binding.progressBar listAdapter = PlaybackStatisticsListAdapter(this) feedStatisticsList.setLayoutManager(LinearLayoutManager(context)) feedStatisticsList.setAdapter(listAdapter) - EventBus.getDefault().register(this) refreshStatistics() return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun onDestroyView() { super.onDestroyView() _binding = null - EventBus.getDefault().unregister(this) - disposable?.dispose() +// disposable?.dispose() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun statisticsEvent(event: StatisticsEvent?) { - refreshStatistics() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.StatisticsEvent -> refreshStatistics() + else -> {} + } + } + } } override fun onPrepareOptionsMenu(menu: Menu) { @@ -81,7 +94,23 @@ class SubscriptionStatisticsFragment : Fragment() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == R.id.statistics_filter) { if (statisticsResult != null) { - StatisticsFilterDialog(requireContext(), statisticsResult!!.oldestDate).show() + val dialog = object: DatesFilterDialog(requireContext(), statisticsResult!!.oldestDate) { + override fun initParams() { + prefs = requireContext().getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE) + includeMarkedAsPlayed = prefs!!.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false) + timeFilterFrom = prefs!!.getLong(StatisticsFragment.PREF_FILTER_FROM, 0) + timeFilterTo = prefs!!.getLong(StatisticsFragment.PREF_FILTER_TO, Long.MAX_VALUE) + } + override fun callback(timeFilterFrom: Long, timeFilterTo: Long, includeMarkedAsPlayed: Boolean) { + prefs!!.edit() + .putBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, includeMarkedAsPlayed) + .putLong(StatisticsFragment.PREF_FILTER_FROM, timeFilterFrom) + .putLong(StatisticsFragment.PREF_FILTER_TO, timeFilterTo) + .apply() + EventFlow.postEvent(FlowEvent.StatisticsEvent()) + } + } + dialog.show() } return true } @@ -95,34 +124,57 @@ class SubscriptionStatisticsFragment : Fragment() { } private fun loadStatistics() { - if (disposable != null) { - disposable!!.dispose() - } +// disposable?.dispose() + val prefs = requireContext().getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE) val includeMarkedAsPlayed = prefs.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false) val timeFilterFrom = prefs.getLong(StatisticsFragment.PREF_FILTER_FROM, 0) val timeFilterTo = prefs.getLong(StatisticsFragment.PREF_FILTER_TO, Long.MAX_VALUE) - disposable = Observable.fromCallable { - val statisticsData = DBReader.getStatistics( - includeMarkedAsPlayed, timeFilterFrom, timeFilterTo) - statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> - item2.timePlayed.compareTo(item1.timePlayed) - } - statisticsData - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ result: StatisticsResult -> - statisticsResult = result + +// disposable = Observable.fromCallable { +// val statisticsData = DBReader.getStatistics( +// includeMarkedAsPlayed, timeFilterFrom, timeFilterTo) +// statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> +// item2.timePlayed.compareTo(item1.timePlayed) +// } +// statisticsData +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ result: StatisticsResult -> +// statisticsResult = result +// // When "from" is "today", set it to today +// listAdapter.setTimeFilter(includeMarkedAsPlayed, max( +// min(timeFilterFrom.toDouble(), System.currentTimeMillis().toDouble()), result.oldestDate.toDouble()) +// .toLong(), +// min(timeFilterTo.toDouble(), System.currentTimeMillis().toDouble()).toLong()) +// listAdapter.update(result.feedTime) +// progressBar.visibility = View.GONE +// feedStatisticsList.visibility = View.VISIBLE +// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + + lifecycleScope.launch { + try { + val statisticsData = withContext(Dispatchers.IO) { + val data = DBReader.getStatistics(includeMarkedAsPlayed, timeFilterFrom, timeFilterTo) + data.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem -> + item2.timePlayed.compareTo(item1.timePlayed) + } + data + } + statisticsResult = statisticsData // When "from" is "today", set it to today - listAdapter.setTimeFilter(includeMarkedAsPlayed, max( - min(timeFilterFrom.toDouble(), System.currentTimeMillis().toDouble()), result.oldestDate.toDouble()) - .toLong(), + listAdapter.setTimeFilter(includeMarkedAsPlayed, + max(min(timeFilterFrom.toDouble(), System.currentTimeMillis().toDouble()), statisticsData.oldestDate.toDouble()).toLong(), min(timeFilterTo.toDouble(), System.currentTimeMillis().toDouble()).toLong()) - listAdapter.update(result.feedTime) + listAdapter.update(statisticsData.feedTime) progressBar.visibility = View.GONE feedStatisticsList.visibility = View.VISIBLE - }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + } catch (error: Throwable) { + // This also runs on the Main thread + Log.e(TAG, Log.getStackTraceString(error)) + } + } } companion object { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/statistics/years/YearsStatisticsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/statistics/years/YearsStatisticsFragment.kt index 6c85be9c..74ecbc42 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/statistics/years/YearsStatisticsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/statistics/years/YearsStatisticsFragment.kt @@ -5,7 +5,8 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.StatisticsFragmentBinding import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.storage.DBReader.MonthlyStatisticsItem -import ac.mdiq.podcini.util.event.StatisticsEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.os.Bundle import android.util.Log import android.view.LayoutInflater @@ -14,15 +15,13 @@ import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext /** * Displays the yearly statistics screen @@ -31,7 +30,7 @@ class YearsStatisticsFragment : Fragment() { private var _binding: StatisticsFragmentBinding? = null private val binding get() = _binding!! - private var disposable: Disposable? = null +// private var disposable: Disposable? = null private lateinit var yearStatisticsList: RecyclerView private lateinit var progressBar: ProgressBar @@ -46,22 +45,32 @@ class YearsStatisticsFragment : Fragment() { listAdapter = YearStatisticsListAdapter(requireContext()) yearStatisticsList.layoutManager = LinearLayoutManager(context) yearStatisticsList.adapter = listAdapter - EventBus.getDefault().register(this) refreshStatistics() return binding.root } + override fun onStart() { + super.onStart() + procFlowEvents() + } + override fun onDestroyView() { super.onDestroyView() _binding = null - EventBus.getDefault().unregister(this) - disposable?.dispose() + +// disposable?.dispose() } - @Subscribe(threadMode = ThreadMode.MAIN) - fun statisticsEvent(event: StatisticsEvent?) { - refreshStatistics() + private fun procFlowEvents() { + lifecycleScope.launch { + EventFlow.events.collectLatest { event -> + when (event) { + is FlowEvent.StatisticsEvent -> refreshStatistics() + else -> {} + } + } + } } @Deprecated("Deprecated in Java") @@ -78,16 +87,30 @@ class YearsStatisticsFragment : Fragment() { } private fun loadStatistics() { - disposable?.dispose() +// disposable?.dispose() - disposable = Observable.fromCallable { DBReader.getMonthlyTimeStatistics() } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ result: List -> +// disposable = Observable.fromCallable { DBReader.getMonthlyTimeStatistics() } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe({ result: List -> +// listAdapter.update(result) +// progressBar.visibility = View.GONE +// yearStatisticsList.visibility = View.VISIBLE +// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + + lifecycleScope.launch { + try { + val result: List = withContext(Dispatchers.IO) { + DBReader.getMonthlyTimeStatistics() + } listAdapter.update(result) progressBar.visibility = View.GONE yearStatisticsList.visibility = View.VISIBLE - }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) + } catch (error: Throwable) { + // This also runs on the Main thread + Log.e(TAG, Log.getStackTraceString(error)) + } + } } companion object { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt b/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt index 47027206..64141d5b 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt @@ -21,7 +21,6 @@ import ac.mdiq.podcini.databinding.FeeditemlistItemBinding import ac.mdiq.podcini.ui.adapter.CoverLoader import ac.mdiq.podcini.feed.util.ImageResourceUtils import ac.mdiq.podcini.net.download.MediaSizeLoader -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.storage.model.playback.MediaType @@ -35,6 +34,7 @@ import ac.mdiq.podcini.ui.actions.actionbutton.TTSActionButton import ac.mdiq.podcini.ui.view.CircularProgressBar import ac.mdiq.podcini.ui.utils.ThemeUtils import ac.mdiq.podcini.util.* +import ac.mdiq.podcini.util.event.FlowEvent import android.widget.LinearLayout import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat.getDrawable @@ -45,7 +45,7 @@ import kotlin.math.max * Holds the view which shows FeedItems. */ @UnstableApi -class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGroup?) : +open class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGroup?) : RecyclerView.ViewHolder(LayoutInflater.from(activity).inflate(R.layout.feeditemlist_item, parent, false)) { val binding: FeeditemlistItemBinding = FeeditemlistItemBinding.bind(itemView) @@ -56,7 +56,7 @@ class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGrou private val placeholder: TextView = binding.txtvPlaceholder private val cover: ImageView = binding.imgvCover private val title: TextView = binding.txtvTitle - private val pubDate: TextView = binding.txtvPubDate + protected val pubDate: TextView = binding.txtvPubDate private val position: TextView = binding.txtvPosition private val duration: TextView = binding.txtvDuration private val size: TextView = binding.size @@ -100,8 +100,9 @@ class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGrou leftPadding.contentDescription = item.title binding.playedMark.visibility = View.GONE } - pubDate.text = DateFormatter.formatAbbrev(activity, item.getPubDate()) - pubDate.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate())) + + setPubDate(item) + isFavorite.visibility = if (item.isTagged(FeedItem.TAG_FAVORITE)) View.VISIBLE else View.GONE isInQueue.visibility = if (item.isTagged(FeedItem.TAG_QUEUE)) View.VISIBLE else View.GONE container.alpha = if (item.isPlayed()) 0.75f else 1.0f @@ -152,6 +153,11 @@ class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGrou } } + open fun setPubDate(item: FeedItem) { + pubDate.text = DateFormatter.formatAbbrev(activity, item.getPubDate()) + pubDate.setContentDescription(DateFormatter.formatForAccessibility(item.getPubDate())) + } + private fun bind(media: FeedMedia) { isVideo.visibility = if (media.getMediaType() == MediaType.VIDEO) View.VISIBLE else View.GONE duration.visibility = if (media.getDuration() > 0) View.VISIBLE else View.GONE @@ -246,7 +252,7 @@ class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGrou } } - private fun updateDuration(event: PlaybackPositionEvent) { + private fun updateDuration(event: FlowEvent.PlaybackPositionEvent) { val media = feedItem?.media if (media != null) { media.setPosition(event.position) @@ -270,7 +276,7 @@ class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGrou val isCurrentlyPlayingItem: Boolean get() = item?.media != null && PlaybackStatus.isCurrentlyPlaying(item?.media) - fun notifyPlaybackPositionUpdated(event: PlaybackPositionEvent) { + fun notifyPlaybackPositionUpdated(event: FlowEvent.PlaybackPositionEvent) { progressBar.progress = (100.0 * event.position / event.duration).toInt() position.text = Converter.getDurationStringLong(event.position) updateDuration(event) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt b/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt index 5dbad339..dd43279f 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt @@ -14,10 +14,13 @@ import ac.mdiq.podcini.ui.activity.appstartintent.PlaybackSpeedActivityStarter import ac.mdiq.podcini.ui.activity.appstartintent.VideoPlayerActivityStarter import ac.mdiq.podcini.util.Converter.getDurationStringLong import ac.mdiq.podcini.util.TimeSpeedConverter +import android.R.attr.bitmap import android.appwidget.AppWidgetManager import android.content.ComponentName import android.content.Context import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Matrix import android.graphics.drawable.BitmapDrawable import android.util.Log import android.view.KeyEvent @@ -30,8 +33,10 @@ import coil.request.SuccessResult import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import kotlin.math.max + /** * Updates the state of the player widget. */ @@ -99,10 +104,17 @@ object WidgetUpdater { }) .size(iconSize, iconSize) .build() - val result = (context.imageLoader.execute(request) as SuccessResult).drawable - icon = (result as BitmapDrawable).bitmap - if (icon != null) views.setImageViewBitmap(R.id.imgvCover, icon) - else views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher) + withContext(Dispatchers.Main) { + val result = (context.imageLoader.execute(request) as SuccessResult).drawable + icon = (result as BitmapDrawable).bitmap + try { + if (icon != null) views.setImageViewBitmap(R.id.imgvCover, icon) + else views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher) + } catch(e: Exception) { + Log.e(TAG, e.message?:"") + e.printStackTrace() + } + } } } catch (tr1: Throwable) { Log.e(TAG, "Error loading the media icon for the widget", tr1) diff --git a/app/src/main/java/ac/mdiq/podcini/util/FeedItemPermutors.kt b/app/src/main/java/ac/mdiq/podcini/util/FeedItemPermutors.kt index 8c951c70..154220d3 100644 --- a/app/src/main/java/ac/mdiq/podcini/util/FeedItemPermutors.kt +++ b/app/src/main/java/ac/mdiq/podcini/util/FeedItemPermutors.kt @@ -44,6 +44,19 @@ object FeedItemPermutors { SortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> itemLink(f2).compareTo(itemLink(f1)) } + SortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> + playDate(f1).compareTo(playDate(f2)) + } + SortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> + playDate(f2).compareTo(playDate(f1)) + } + SortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> + completeDate(f1).compareTo(completeDate(f2)) + } + SortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> + completeDate(f2).compareTo(completeDate(f1)) + } + SortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> feedTitle(f1).compareTo(feedTitle(f2)) } @@ -77,28 +90,35 @@ object FeedItemPermutors { // Null-safe accessors private fun pubDate(item: FeedItem?): Date { - return if (item?.pubDate != null) item.pubDate!! else Date(0) + return item?.pubDate ?: Date(0) + } + + private fun playDate(item: FeedItem?): Long { + return item?.media?.getLastPlayedTime() ?: 0 + } + + private fun completeDate(item: FeedItem?): Date { + return item?.media?.getPlaybackCompletionDate() ?: Date(0) } private fun itemTitle(item: FeedItem?): String { - return if (item?.title != null) item.title!!.lowercase(Locale.getDefault()) else "" + return (item?.title ?: "").lowercase(Locale.getDefault()) } private fun duration(item: FeedItem?): Int { - return if (item?.media != null) item.media!!.getDuration() else 0 + return item?.media?.getDuration() ?: 0 } private fun size(item: FeedItem?): Long { - return if (item?.media != null) item.media!!.size else 0 + return item?.media?.size ?: 0 } private fun itemLink(item: FeedItem?): String { - return if (item?.link != null) item.link!!.lowercase(Locale.getDefault()) else "" + return (item?.link ?: "").lowercase(Locale.getDefault()) } private fun feedTitle(item: FeedItem?): String { - Logd("permutors", "feedTitle ${item?.feed?.title}") - return if (item?.feed?.title != null) item.feed!!.title!!.lowercase(Locale.getDefault()) else "" + return (item?.feed?.title ?: "").lowercase(Locale.getDefault()) } /** diff --git a/app/src/main/java/ac/mdiq/podcini/util/NetworkUtils.kt b/app/src/main/java/ac/mdiq/podcini/util/NetworkUtils.kt index 68e9314e..401fad08 100644 --- a/app/src/main/java/ac/mdiq/podcini/util/NetworkUtils.kt +++ b/app/src/main/java/ac/mdiq/podcini/util/NetworkUtils.kt @@ -131,8 +131,8 @@ object NetworkUtils { val bufferedReader = BufferedReader(InputStreamReader(inputStream)) val stringBuilder = StringBuilder() - var line: String? - while (bufferedReader.readLine().also { line = it } != null) { + var line = "" + while (bufferedReader.readLine()?.also { line = it } != null) { stringBuilder.append(line) } diff --git a/app/src/main/java/ac/mdiq/podcini/util/comparator/PlaybackLastPlayedDateComparator.kt b/app/src/main/java/ac/mdiq/podcini/util/comparator/PlaybackLastPlayedDateComparator.kt new file mode 100644 index 00000000..e6a869cf --- /dev/null +++ b/app/src/main/java/ac/mdiq/podcini/util/comparator/PlaybackLastPlayedDateComparator.kt @@ -0,0 +1,12 @@ +package ac.mdiq.podcini.util.comparator + +import ac.mdiq.podcini.storage.model.feed.FeedItem + +class PlaybackLastPlayedDateComparator : Comparator { + override fun compare(lhs: FeedItem, rhs: FeedItem): Int { + if (lhs.media?.getLastPlayedTime() != null && rhs.media?.getLastPlayedTime() != null) + return rhs.media!!.getLastPlayedTime().compareTo(lhs.media!!.getLastPlayedTime()) + + return 0 + } +} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/DiscoveryDefaultUpdateEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/DiscoveryDefaultUpdateEvent.kt deleted file mode 100644 index 5e1055de..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/DiscoveryDefaultUpdateEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event - -class DiscoveryDefaultUpdateEvent diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/DownloadLogEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/DownloadLogEvent.kt deleted file mode 100644 index 3a47ec79..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/DownloadLogEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package ac.mdiq.podcini.util.event - -class DownloadLogEvent private constructor() { - override fun toString(): String { - return "DownloadLogEvent" - } - - companion object { - @JvmStatic - fun listUpdated(): DownloadLogEvent { - return DownloadLogEvent() - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/EpisodeDownloadEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/EpisodeDownloadEvent.kt deleted file mode 100644 index 6363ae0b..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/EpisodeDownloadEvent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package ac.mdiq.podcini.util.event - -import ac.mdiq.podcini.storage.model.download.DownloadStatus - -class EpisodeDownloadEvent(private val map: Map) { - val urls: Set - get() = map.keys -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/FavoritesEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/FavoritesEvent.kt deleted file mode 100644 index fcebabfd..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/FavoritesEvent.kt +++ /dev/null @@ -1,4 +0,0 @@ -package ac.mdiq.podcini.util.event - -//TODO: need to specify ids -class FavoritesEvent diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/FeedItemEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/FeedItemEvent.kt deleted file mode 100644 index fa9a0fd3..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/FeedItemEvent.kt +++ /dev/null @@ -1,18 +0,0 @@ -package ac.mdiq.podcini.util.event - -import ac.mdiq.podcini.storage.model.feed.FeedItem - - -// TODO: this appears not being posted -class FeedItemEvent(@JvmField val items: List) { - companion object { - fun updated(items: List): FeedItemEvent { - return FeedItemEvent(items) - } - - @JvmStatic - fun updated(vararg items: FeedItem): FeedItemEvent { - return FeedItemEvent(listOf(*items)) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/FeedListUpdateEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/FeedListUpdateEvent.kt deleted file mode 100644 index 7ea6bef9..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/FeedListUpdateEvent.kt +++ /dev/null @@ -1,25 +0,0 @@ -package ac.mdiq.podcini.util.event - -import ac.mdiq.podcini.storage.model.feed.Feed - -class FeedListUpdateEvent { - private val feeds: MutableList = ArrayList() - - constructor(feeds: List) { - for (feed in feeds) { - this.feeds.add(feed.id) - } - } - - constructor(feed: Feed) { - feeds.add(feed.id) - } - - constructor(feedId: Long) { - feeds.add(feedId) - } - - fun contains(feed: Feed): Boolean { - return feeds.contains(feed.id) - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/FeedTagsChangedEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/FeedTagsChangedEvent.kt deleted file mode 100644 index 10445bad..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/FeedTagsChangedEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event - -class FeedTagsChangedEvent \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/FeedUpdateRunningEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/FeedUpdateRunningEvent.kt deleted file mode 100644 index a5c676f1..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/FeedUpdateRunningEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event - -class FeedUpdateRunningEvent(@JvmField val isFeedUpdateRunning: Boolean) diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/FlowEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/FlowEvent.kt new file mode 100644 index 00000000..e8c0734a --- /dev/null +++ b/app/src/main/java/ac/mdiq/podcini/util/event/FlowEvent.kt @@ -0,0 +1,241 @@ +package ac.mdiq.podcini.util.event + +import ac.mdiq.podcini.storage.model.download.DownloadStatus +import ac.mdiq.podcini.storage.model.feed.Feed +import ac.mdiq.podcini.storage.model.feed.FeedItem +import ac.mdiq.podcini.storage.model.feed.SortOrder +import ac.mdiq.podcini.storage.model.feed.VolumeAdaptionSetting +import ac.mdiq.podcini.util.Logd +import android.content.Context +import android.view.KeyEvent +import androidx.core.util.Consumer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import org.apache.commons.lang3.builder.ToStringBuilder +import org.apache.commons.lang3.builder.ToStringStyle +import java.util.* +import kotlin.math.abs +import kotlin.math.max + +sealed class FlowEvent { + data class PlaybackPositionEvent(val position: Int, val duration: Int) : FlowEvent() + + data class PlaybackServiceEvent(val action: Action) : FlowEvent() { + enum class Action { + SERVICE_STARTED, + SERVICE_SHUT_DOWN, + } + } + + data class BufferUpdateEvent(val progress: Float) : FlowEvent() { + fun hasStarted(): Boolean { + return progress == PROGRESS_STARTED + } + + fun hasEnded(): Boolean { + return progress == PROGRESS_ENDED + } + + companion object { + private const val PROGRESS_STARTED = -1f + private const val PROGRESS_ENDED = -2f + fun started(): BufferUpdateEvent { + return BufferUpdateEvent(PROGRESS_STARTED) + } + + fun ended(): BufferUpdateEvent { + return BufferUpdateEvent(PROGRESS_ENDED) + } + + fun progressUpdate(progress: Float): BufferUpdateEvent { + return BufferUpdateEvent(progress) + } + } + } + + data class HistoryEvent(val sortOrder: SortOrder = SortOrder.PLAYED_DATE_NEW_OLD, val startDate: Long = 0, val endDate: Long = Date().time) : FlowEvent() + + data class SleepTimerUpdatedEvent(private val timeLeft: Long) : FlowEvent() { + fun getTimeLeft(): Long { + return abs(timeLeft.toDouble()).toLong() + } + + val isOver: Boolean + get() = timeLeft == 0L + + fun wasJustEnabled(): Boolean { + return timeLeft < 0 + } + + val isCancelled: Boolean + get() = timeLeft == CANCELLED + + companion object { + private const val CANCELLED = Long.MAX_VALUE + fun justEnabled(timeLeft: Long): SleepTimerUpdatedEvent { + return SleepTimerUpdatedEvent(-timeLeft) + } + + fun updated(timeLeft: Long): SleepTimerUpdatedEvent { + return SleepTimerUpdatedEvent(max(0.0, timeLeft.toDouble()).toLong()) + } + + fun cancelled(): SleepTimerUpdatedEvent { + return SleepTimerUpdatedEvent(CANCELLED) + } + } + } + + data class SpeedChangedEvent(val newSpeed: Float) : FlowEvent() + + data class StartPlayEvent(val item: FeedItem) : FlowEvent() + + data class SkipIntroEndingChangedEvent(val skipIntro: Int, val skipEnding: Int, val feedId: Long) : FlowEvent() + + data class SpeedPresetChangedEvent(val speed: Float, val feedId: Long) : FlowEvent() + + data class VolumeAdaptionChangedEvent(val volumeAdaptionSetting: VolumeAdaptionSetting, val feedId: Long) : FlowEvent() + + data class DiscoveryCompletedEvent(val dummy: Unit = Unit) : FlowEvent() + + data class DownloadLogEvent(val dummy: Unit = Unit) : FlowEvent() + + data class EpisodeDownloadEvent(private val map: Map) : FlowEvent() { + val urls: Set + get() = map.keys + } + + data class FavoritesEvent(val dummy: Unit = Unit) : FlowEvent() + + data class FeedItemEvent(val items: List) : FlowEvent() { + companion object { + fun updated(items: List): FeedItemEvent { + return FeedItemEvent(items) + } + + @JvmStatic + fun updated(vararg items: FeedItem): FeedItemEvent { + return FeedItemEvent(listOf(*items)) + } + } + } + + data class FeedListUpdateEvent(val feedIds: List = emptyList()) : FlowEvent() { + constructor(feed: Feed) : this(listOf(feed.id)) + constructor(feedId: Long) : this(listOf(feedId)) + constructor(feeds: List, junkInfo: String = "") : this(feeds.map { it.id }) + + fun contains(feed: Feed): Boolean { + return feedIds.contains(feed.id) + } + } + + data class FeedTagsChangedEvent(val dummy: Unit = Unit) : FlowEvent() + + data class FeedUpdateRunningEvent(val isFeedUpdateRunning: Boolean) : FlowEvent() + + data class MessageEvent @JvmOverloads constructor(val message: String, val action: Consumer? = null, val actionText: String? = null) : FlowEvent() + + data class PlayerErrorEvent(val message: String) : FlowEvent() + + data class PlayerStatusEvent(val dummy: Unit = Unit) : FlowEvent() + + data class QueueEvent(val action: Action, val item: FeedItem?, val items: List, val position: Int) : FlowEvent() { + + enum class Action { + ADDED, ADDED_ITEMS, SET_QUEUE, REMOVED, IRREVERSIBLE_REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED + } + + companion object { + @JvmStatic + fun added(item: FeedItem, position: Int): QueueEvent { + return QueueEvent(Action.ADDED, item, listOf(), position) + } + + @JvmStatic + fun setQueue(queue: List): QueueEvent { + return QueueEvent(Action.SET_QUEUE, null, queue, -1) + } + + @JvmStatic + fun removed(item: FeedItem): QueueEvent { + return QueueEvent(Action.REMOVED, item, listOf(), -1) + } + + @JvmStatic + fun irreversibleRemoved(item: FeedItem): QueueEvent { + return QueueEvent(Action.IRREVERSIBLE_REMOVED, item, listOf(), -1) + } + + @JvmStatic + fun cleared(): QueueEvent { + return QueueEvent(Action.CLEARED, null, listOf(), -1) + } + + @JvmStatic + fun sorted(sortedQueue: List): QueueEvent { + return QueueEvent(Action.SORTED, null, sortedQueue, -1) + } + + @JvmStatic + fun moved(item: FeedItem, newPosition: Int): QueueEvent { + return QueueEvent(Action.MOVED, item, listOf(), newPosition) + } + } + } + + data class StatisticsEvent(val dummy: Unit = Unit) : FlowEvent() + + data class SwipeActionsChangedEvent(val dummy: Unit = Unit) : FlowEvent() + + data class SyncServiceEvent(val messageResId: Int, val message: String = "") : FlowEvent() + + data class UnreadItemsUpdateEvent(val dummy: Unit = Unit) : FlowEvent() + + data class DiscoveryDefaultUpdateEvent(val dummy: Unit = Unit) : FlowEvent() + + data class FeedEvent(private val action: Action, val feedId: Long) : FlowEvent() { + enum class Action { + FILTER_CHANGED, + SORT_ORDER_CHANGED + } + + override fun toString(): String { + return ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("action", action) + .append("feedId", feedId) + .toString() + } + } + + data class AllEpisodesFilterChangedEvent(val filterValues: Set?) : FlowEvent() +} + +object EventFlow { + val collectorCount = MutableStateFlow(0) + private val _events = MutableSharedFlow(replay = 0) + val events: SharedFlow = _events + private val _stickyEvents = MutableSharedFlow(replay = 1) + val stickyEvents: SharedFlow = _stickyEvents + private val _keyEvents = MutableSharedFlow(replay = 0) + val keyEvents: SharedFlow = _keyEvents + + init {} + + fun postEvent(event: FlowEvent) = GlobalScope.launch(Dispatchers.Default) { + val stat = _events.emit(event) + Logd("EventFlow", "event posted: $event $stat") + } + + fun postStickyEvent(event: FlowEvent) = GlobalScope.launch(Dispatchers.Default) { + val stat = _stickyEvents.emit(event) + Logd("EventFlow", "sticky event posted: $event $stat") + } + + fun postEvent(event: KeyEvent) = GlobalScope.launch(Dispatchers.Default) { + val stat = _keyEvents.emit(event) + Logd("EventFlow", "key event posted: $event $stat") + } +} \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/MessageEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/MessageEvent.kt deleted file mode 100644 index 4c3c4769..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/MessageEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ac.mdiq.podcini.util.event - -import android.content.Context -import androidx.core.util.Consumer - -class MessageEvent @JvmOverloads constructor(@JvmField val message: String, - @JvmField val action: Consumer? = null, - @JvmField val actionText: String? = null -) diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/PlayerErrorEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/PlayerErrorEvent.kt deleted file mode 100644 index a3e53561..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/PlayerErrorEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event - -class PlayerErrorEvent(@JvmField val message: String) diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/PlayerStatusEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/PlayerStatusEvent.kt deleted file mode 100644 index dd93179d..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/PlayerStatusEvent.kt +++ /dev/null @@ -1,4 +0,0 @@ -package ac.mdiq.podcini.util.event - -//TODO: need to be optimized -class PlayerStatusEvent diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/QueueEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/QueueEvent.kt deleted file mode 100644 index c0ccc64a..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/QueueEvent.kt +++ /dev/null @@ -1,50 +0,0 @@ -package ac.mdiq.podcini.util.event - -import ac.mdiq.podcini.storage.model.feed.FeedItem - -class QueueEvent private constructor(@JvmField val action: Action, - @JvmField val item: FeedItem?, - @JvmField val items: List, - @JvmField val position: Int) { - - enum class Action { - ADDED, ADDED_ITEMS, SET_QUEUE, REMOVED, IRREVERSIBLE_REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED - } - - companion object { - @JvmStatic - fun added(item: FeedItem, position: Int): QueueEvent { - return QueueEvent(Action.ADDED, item, listOf(), position) - } - - @JvmStatic - fun setQueue(queue: List): QueueEvent { - return QueueEvent(Action.SET_QUEUE, null, queue, -1) - } - - @JvmStatic - fun removed(item: FeedItem): QueueEvent { - return QueueEvent(Action.REMOVED, item, listOf(), -1) - } - - @JvmStatic - fun irreversibleRemoved(item: FeedItem): QueueEvent { - return QueueEvent(Action.IRREVERSIBLE_REMOVED, item, listOf(), -1) - } - - @JvmStatic - fun cleared(): QueueEvent { - return QueueEvent(Action.CLEARED, null, listOf(), -1) - } - - @JvmStatic - fun sorted(sortedQueue: List): QueueEvent { - return QueueEvent(Action.SORTED, null, sortedQueue, -1) - } - - @JvmStatic - fun moved(item: FeedItem, newPosition: Int): QueueEvent { - return QueueEvent(Action.MOVED, item, listOf(), newPosition) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/StatisticsEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/StatisticsEvent.kt deleted file mode 100644 index f198c1ce..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/StatisticsEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event - -class StatisticsEvent diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/SwipeActionsChangedEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/SwipeActionsChangedEvent.kt deleted file mode 100644 index 94e89048..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/SwipeActionsChangedEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event - -class SwipeActionsChangedEvent diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/SyncServiceEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/SyncServiceEvent.kt deleted file mode 100644 index 7a1db298..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/SyncServiceEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event - -class SyncServiceEvent(@JvmField val messageResId: Int, val message: String = "") diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/UnreadItemsUpdateEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/UnreadItemsUpdateEvent.kt deleted file mode 100644 index afbdd852..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/UnreadItemsUpdateEvent.kt +++ /dev/null @@ -1,4 +0,0 @@ -package ac.mdiq.podcini.util.event - -// TODO: need to specify ids -class UnreadItemsUpdateEvent diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/playback/BufferUpdateEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/playback/BufferUpdateEvent.kt deleted file mode 100644 index 82fc63dd..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/playback/BufferUpdateEvent.kt +++ /dev/null @@ -1,27 +0,0 @@ -package ac.mdiq.podcini.util.event.playback - -class BufferUpdateEvent private constructor(@JvmField val progress: Float) { - fun hasStarted(): Boolean { - return progress == PROGRESS_STARTED - } - - fun hasEnded(): Boolean { - return progress == PROGRESS_ENDED - } - - companion object { - private const val PROGRESS_STARTED = -1f - private const val PROGRESS_ENDED = -2f - fun started(): BufferUpdateEvent { - return BufferUpdateEvent(PROGRESS_STARTED) - } - - fun ended(): BufferUpdateEvent { - return BufferUpdateEvent(PROGRESS_ENDED) - } - - fun progressUpdate(progress: Float): BufferUpdateEvent { - return BufferUpdateEvent(progress) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackHistoryEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackHistoryEvent.kt deleted file mode 100644 index 2aec0a40..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackHistoryEvent.kt +++ /dev/null @@ -1,14 +0,0 @@ -package ac.mdiq.podcini.util.event.playback - -class PlaybackHistoryEvent private constructor() { - override fun toString(): String { - return "PlaybackHistoryEvent" - } - - companion object { - @JvmStatic - fun listUpdated(): PlaybackHistoryEvent { - return PlaybackHistoryEvent() - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackPositionEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackPositionEvent.kt deleted file mode 100644 index b217c909..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackPositionEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event.playback - -class PlaybackPositionEvent(@JvmField val position: Int, @JvmField val duration: Int) diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackServiceEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackServiceEvent.kt deleted file mode 100644 index 8fcea8eb..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackServiceEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package ac.mdiq.podcini.util.event.playback - -class PlaybackServiceEvent(@JvmField val action: Action) { - enum class Action { - SERVICE_STARTED, - SERVICE_SHUT_DOWN, -// SERVICE_RESTARTED - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/playback/SleepTimerUpdatedEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/playback/SleepTimerUpdatedEvent.kt deleted file mode 100644 index a317a9d4..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/playback/SleepTimerUpdatedEvent.kt +++ /dev/null @@ -1,35 +0,0 @@ -package ac.mdiq.podcini.util.event.playback - -import kotlin.math.abs -import kotlin.math.max - -class SleepTimerUpdatedEvent private constructor(private val timeLeft: Long) { - fun getTimeLeft(): Long { - return abs(timeLeft.toDouble()).toLong() - } - - val isOver: Boolean - get() = timeLeft == 0L - - fun wasJustEnabled(): Boolean { - return timeLeft < 0 - } - - val isCancelled: Boolean - get() = timeLeft == CANCELLED - - companion object { - private const val CANCELLED = Long.MAX_VALUE - fun justEnabled(timeLeft: Long): SleepTimerUpdatedEvent { - return SleepTimerUpdatedEvent(-timeLeft) - } - - fun updated(timeLeft: Long): SleepTimerUpdatedEvent { - return SleepTimerUpdatedEvent(max(0.0, timeLeft.toDouble()).toLong()) - } - - fun cancelled(): SleepTimerUpdatedEvent { - return SleepTimerUpdatedEvent(CANCELLED) - } - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/playback/SpeedChangedEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/playback/SpeedChangedEvent.kt deleted file mode 100644 index fc5eee47..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/playback/SpeedChangedEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event.playback - -class SpeedChangedEvent(@JvmField val newSpeed: Float) diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/playback/StartPlayEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/playback/StartPlayEvent.kt deleted file mode 100644 index 2a33821b..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/playback/StartPlayEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package ac.mdiq.podcini.util.event.playback - -import ac.mdiq.podcini.storage.model.feed.FeedItem - -class StartPlayEvent(@JvmField val item: FeedItem) \ No newline at end of file diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/settings/SkipIntroEndingChangedEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/settings/SkipIntroEndingChangedEvent.kt deleted file mode 100644 index afae5b29..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/settings/SkipIntroEndingChangedEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event.settings - -class SkipIntroEndingChangedEvent(val skipIntro: Int, val skipEnding: Int, val feedId: Long) diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/settings/SpeedPresetChangedEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/settings/SpeedPresetChangedEvent.kt deleted file mode 100644 index 18bbf1bb..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/settings/SpeedPresetChangedEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package ac.mdiq.podcini.util.event.settings - -class SpeedPresetChangedEvent(val speed: Float, val feedId: Long) diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/settings/VolumeAdaptionChangedEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/settings/VolumeAdaptionChangedEvent.kt deleted file mode 100644 index 0b33e240..00000000 --- a/app/src/main/java/ac/mdiq/podcini/util/event/settings/VolumeAdaptionChangedEvent.kt +++ /dev/null @@ -1,5 +0,0 @@ -package ac.mdiq.podcini.util.event.settings - -import ac.mdiq.podcini.storage.model.feed.VolumeAdaptionSetting - -class VolumeAdaptionChangedEvent(val volumeAdaptionSetting: VolumeAdaptionSetting, val feedId: Long) diff --git a/app/src/main/res/layout/statistics_filter_dialog.xml b/app/src/main/res/layout/statistics_filter_dialog.xml index 684ceabd..566a0a57 100644 --- a/app/src/main/res/layout/statistics_filter_dialog.xml +++ b/app/src/main/res/layout/statistics_filter_dialog.xml @@ -87,6 +87,7 @@ + + + + + + app:showAsAction="never"/> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7e1dc3f3..66b3fefa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,7 +21,7 @@ Subscriptions Subscriptions list Cancel download - Playback history + History Episode cache full The episode cache limit has been reached. You can increase the cache size in the Settings. Years @@ -352,6 +352,8 @@ Sort Keep sorted Date + Played date + Completed date Duration Episode title Podcast title diff --git a/app/src/play/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt b/app/src/play/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt index d61e4c6c..66bfeb64 100644 --- a/app/src/play/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt +++ b/app/src/play/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt @@ -8,8 +8,8 @@ import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.storage.model.playback.RemoteMedia import ac.mdiq.podcini.util.Logd -import ac.mdiq.podcini.util.event.PlayerErrorEvent -import ac.mdiq.podcini.util.event.playback.BufferUpdateEvent +import ac.mdiq.podcini.util.event.EventFlow +import ac.mdiq.podcini.util.event.FlowEvent import android.annotation.SuppressLint import android.content.Context import android.util.Log @@ -21,7 +21,6 @@ import com.google.android.gms.cast.framework.CastState import com.google.android.gms.cast.framework.media.RemoteMediaClient import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import org.greenrobot.eventbus.EventBus import java.util.concurrent.atomic.AtomicBoolean import kotlin.concurrent.Volatile import kotlin.math.max @@ -67,7 +66,7 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas } override fun onMediaError(mediaError: MediaError) { - EventBus.getDefault().post(PlayerErrorEvent(mediaError.reason!!)) + EventFlow.postEvent(FlowEvent.PlayerErrorEvent(mediaError.reason!!)) } } @@ -82,8 +81,8 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas private fun setBuffering(buffering: Boolean) { when { - buffering && isBuffering.compareAndSet(false, true) -> EventBus.getDefault().post(BufferUpdateEvent.started()) - !buffering && isBuffering.compareAndSet(true, false) -> EventBus.getDefault().post(BufferUpdateEvent.ended()) + buffering && isBuffering.compareAndSet(false, true) -> EventFlow.postEvent(FlowEvent.BufferUpdateEvent.started()) + !buffering && isBuffering.compareAndSet(true, false) -> EventFlow.postEvent(FlowEvent.BufferUpdateEvent.ended()) } } @@ -188,7 +187,7 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas } MediaStatus.IDLE_REASON_ERROR -> { Log.w(TAG, "Got an error status from the Chromecast. Skipping, if possible, to the next episode...") - EventBus.getDefault().post(PlayerErrorEvent("Chromecast error code 1")) + EventFlow.postEvent(FlowEvent.PlayerErrorEvent("Chromecast error code 1")) endPlayback(false, wasSkipped = false, shouldContinue = true, toStoppedState = true) return } @@ -235,7 +234,7 @@ class CastPsmp(context: Context, callback: MediaPlayerCallback) : MediaPlayerBas private fun playMediaObject(playable: Playable, forceReset: Boolean, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) { if (!CastUtils.isCastable(playable, castContext.sessionManager.currentCastSession)) { Logd(TAG, "media provided is not compatible with cast device") - EventBus.getDefault().post(PlayerErrorEvent("Media not compatible with cast device")) + EventFlow.postEvent(FlowEvent.PlayerErrorEvent("Media not compatible with cast device")) var nextPlayable: Playable? = playable do { nextPlayable = callback.getNextInQueue(nextPlayable) diff --git a/changelog.md b/changelog.md index 1a582f57..e273fdfa 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,14 @@ +## 5.3.0 + +* change normal scope to life cycle scope when possible +* replaced EventBus with Kotlin SharedFlow, be mindful of possible issues +* added sort order based on episode played or completed times, accessible from various episodes' list views +* in history view, dates shown on items are last-played dates +* in history view added sort and date filter +* more conversion of RxJava routines to Kotlin coroutines +* fixed crash issue sometimes when importing OPOL files +* fixed crash issue in About view + ## 5.2.1 * fixed issue of play/pause button not correctly updated in Queue diff --git a/fastlane/metadata/android/en-US/changelogs/3020145.txt b/fastlane/metadata/android/en-US/changelogs/3020145.txt new file mode 100644 index 00000000..9b4c77e9 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020145.txt @@ -0,0 +1,11 @@ + +Version 5.3.0 brings several changes: + +* change normal scope to life cycle scope when possible +* replaced EventBus with Kotlin SharedFlow, be mindful of possible issues +* added sort order based on episode played or completed times, accessible from various episodes' list views +* in history view, dates shown on items are last-played dates +* in history view added sort and date filter +* more conversion of RxJava routines to Kotlin coroutines +* fixed crash issue sometimes when importing OPOL files +* fixed crash issue in About view