5.3.0 commit

This commit is contained in:
Xilin Jia 2024-05-17 23:05:16 +01:00
parent ef5fe709ba
commit c49af77af8
127 changed files with 2761 additions and 1982 deletions

View File

@ -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)

View File

@ -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'

View File

@ -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

View File

@ -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<Feed>())
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) {

View File

@ -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<FeedItemEvent> = ArrayList()
@Subscribe
fun onEvent(event: FeedItemEvent) {
events.add(event)
}
fun getEvents(): List<FeedItemEvent> {
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<FeedItemEventListener?>) {
val feedItemEventListener = FeedItemEventListener()
try {
EventBus.getDefault().register(feedItemEventListener)
consumer.accept(feedItemEventListener)
} finally {
EventBus.getDefault().unregister(feedItemEventListener)
}
}
}
}

View File

@ -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)
}

View File

@ -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()
}
}

View File

@ -12,10 +12,11 @@ import java.util.*
import java.util.concurrent.CountDownLatch
class CombinedSearcher : PodcastSearcher {
override fun search(query: String): Single<List<PodcastSearchResult?>?> {
val disposables = ArrayList<Disposable>()
val singleResults: MutableList<List<PodcastSearchResult?>?> = ArrayList(
Collections.nCopies<List<PodcastSearchResult?>?>(PodcastSearcherRegistry.searchProviders.size, null))
val singleResults: MutableList<List<PodcastSearchResult?>?> =
ArrayList(Collections.nCopies<List<PodcastSearchResult?>?>(PodcastSearcherRegistry.searchProviders.size, null))
val latch = CountDownLatch(PodcastSearcherRegistry.searchProviders.size)
for (i in PodcastSearcherRegistry.searchProviders.indices) {
val searchProviderInfo = PodcastSearcherRegistry.searchProviders[i]

View File

@ -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)
}

View File

@ -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<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag)
Observable.fromFuture(future)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(
{ workInfos: List<WorkInfo> ->
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<WorkInfo> ->
// 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) {

View File

@ -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))

View File

@ -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")

View File

@ -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()
}
}
}
}

View File

@ -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<String> = 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<Long, Long> {
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<EpisodeAction> = 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
}

View File

@ -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() {

View File

@ -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<EpisodeAction> = 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)

View File

@ -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))
}
}

View File

@ -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<Format>
get() {
val formats: MutableList<Format> = 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<Int> { 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<String> { message: String ->
Log.e(TAG, "PlayerErrorEvent: $message")
EventBus.getDefault().postSticky(PlayerErrorEvent(message))
EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(message))
}
}

View File

@ -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) {

View File

@ -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())
}
}

View File

@ -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 {

View File

@ -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, "")

View File

@ -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?) {

View File

@ -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<Preference>(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

View File

@ -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<Preference>(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
}

View File

@ -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<ArrayList<SimpleIconListAdapter.ListItem>?> ->
lifecycleScope.launch(Dispatchers.IO) {
val developers = ArrayList<SimpleIconListAdapter.ListItem>()
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<SimpleIconListAdapter.ListItem>? ->
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()
}
}

View File

@ -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<LicenseItem>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listView.divider = null
licensesLoader = Single.create { emitter: SingleEmitter<ArrayList<LicenseItem>?> ->
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<LicenseItem>? -> 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)

View File

@ -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<ArrayList<SimpleIconListAdapter.ListItem>?> ->
lifecycleScope.launch(Dispatchers.IO) {
val translators = ArrayList<SimpleIconListAdapter.ListItem>()
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<SimpleIconListAdapter.ListItem>? ->
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()
}
}

View File

@ -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<ArrayList<SimpleIconListAdapter.ListItem>?> ->
lifecycleScope.launch(Dispatchers.IO) {
val translators = ArrayList<SimpleIconListAdapter.ListItem>()
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<SimpleIconListAdapter.ListItem>? ->
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()
}
}

View File

@ -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 {

View File

@ -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()

View File

@ -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()

View File

@ -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<FeedItem> {
fun getPlaybackHistory(offset: Int, limit: Int, start: Long = 0L, end: Long = Date().time, sortOrder: SortOrder = SortOrder.PLAYED_DATE_NEW_OLD): List<FeedItem> {
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<String>(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()

View File

@ -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<Long>()))
return resultFeed
}

View File

@ -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<QueueEvent> = ArrayList()
val events: MutableList<FlowEvent.QueueEvent> = ArrayList()
val updatedItems: MutableList<FeedItem> = 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<FeedItem>, events: MutableList<QueueEvent>) {
private fun applySortOrder(queue: MutableList<FeedItem>, events: MutableList<FlowEvent.QueueEvent>) {
// 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<QueueEvent> = ArrayList()
val events: MutableList<FlowEvent.QueueEvent> = ArrayList()
val updatedItems: MutableList<FeedItem> = 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

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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);

View File

@ -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))
}

View File

@ -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))
}

View File

@ -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))
}
}

View File

@ -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())
}
})
}

View File

@ -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

View File

@ -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<OpmlElement>? ->
// 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<OpmlElement>? ->
// 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() {

View File

@ -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) {

View File

@ -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<Feed> = 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)

View File

@ -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<View>(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()
})
}
}

View File

@ -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)

View File

@ -21,7 +21,8 @@ import java.lang.ref.WeakReference
open class EpisodeItemListAdapter(mainActivity: MainActivity) :
SelectableAdapter<EpisodeItemViewHolder?>(mainActivity), View.OnCreateContextMenuListener {
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
private var episodes: List<FeedItem> = ArrayList()
var longPressedItem: FeedItem? = null
private var longPressedPosition: Int = 0 // used to init actionMode

View File

@ -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<String>) {
EventBus.getDefault().post(AllEpisodesFilterChangedEvent(newFilterValues))
EventFlow.postEvent(FlowEvent.AllEpisodesFilterChangedEvent(newFilterValues))
}
class AllEpisodesFilterChangedEvent(val filterValues: Set<String?>?)
companion object {
fun newInstance(filter: FeedItemFilter?): AllEpisodesFilterDialog {
val dialog = AllEpisodesFilterDialog()

View File

@ -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)))
}
}

View File

@ -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()
}

View File

@ -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)

View File

@ -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)

View File

@ -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)
}
}
}
}

View File

@ -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())

View File

@ -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<String>) {
val subscriptionsFilter = SubscriptionsFilter(filterValues.toTypedArray<String>())
UserPreferences.subscriptionsFilter = subscriptionsFilter
EventBus.getDefault().post(UnreadItemsUpdateEvent())
EventFlow.postEvent(FlowEvent.UnreadItemsUpdateEvent())
}
}
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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) {

View File

@ -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<FeedItem> {
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))
}
}

View File

@ -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)

View File

@ -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<FeedItem>())
@ -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
}

View File

@ -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<Any> ->
val media = controller!!.getMedia()
if (media != null) {
loadChapters(media, requireContext(), forceRefresh)
emitter.onSuccess(media)
} else emitter.onComplete()
// disposable = Maybe.create { emitter: MaybeEmitter<Any> ->
// 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) {

View File

@ -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<PodcastSearchResult>? = null
private var topList: List<PodcastSearchResult>? = 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)

View File

@ -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<DownloadResult> = 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()

View File

@ -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<String> = 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())
}
}

View File

@ -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

View File

@ -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<FeedItem> = mutableListOf()
@ -40,6 +43,11 @@ class EpisodesListFragment : BaseEpisodesListFragment() {
return root
}
override fun onStart() {
super.onStart()
procFlowEvents()
}
fun setEpisodes(episodeList_: MutableList<FeedItem>) {
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<FeedItem>): EpisodesListFragment {
val i = EpisodesListFragment()
fun newInstance(episodes: MutableList<FeedItem>): ExternalEpisodesListFragment {
val i = ExternalEpisodesListFragment()
i.setEpisodes(episodes)
return i
}

View File

@ -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)

View File

@ -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<DownloadResult> = 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)

View File

@ -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<Preference>(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
}
}

View File

@ -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<NavDrawerData.FeedDrawerItem>? = 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)

View File

@ -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..<episodes.size) {
episodes[i].id = 1234567890L + i
}
val fragment: Fragment = EpisodesListFragment.newInstance(episodes)
val fragment: Fragment = ExternalEpisodesListFragment.newInstance(episodes)
(activity as MainActivity).loadChildFragment(fragment)
}

View File

@ -1,29 +1,43 @@
package ac.mdiq.podcini.ui.fragment
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.R
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.util.event.playback.PlaybackHistoryEvent
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.actions.menuhandler.MenuItemUtils
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.EpisodeItemListAdapter
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.ItemSortDialog
import ac.mdiq.podcini.ui.statistics.StatisticsFragment
import ac.mdiq.podcini.ui.statistics.subscriptions.DatesFilterDialog
import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder
import ac.mdiq.podcini.util.DateFormatter
import ac.mdiq.podcini.util.Logd
import android.util.Log
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.content.DialogInterface
import android.os.Bundle
import android.view.*
import androidx.annotation.OptIn
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.*
@UnstableApi class PlaybackHistoryFragment : BaseEpisodesListFragment() {
private var sortOrder : SortOrder = SortOrder.PLAYED_DATE_NEW_OLD
private var startDate : Long = 0L
private var endDate : Long = Date().time
class PlaybackHistoryFragment : BaseEpisodesListFragment() {
override fun getFragmentTag(): String {
return "PlaybackHistoryFragment"
}
override fun getPrefName(): String {
return "PlaybackHistoryFragment"
}
@ -38,51 +52,130 @@ class PlaybackHistoryFragment : BaseEpisodesListFragment() {
emptyView.setIcon(R.drawable.ic_history)
emptyView.setTitle(R.string.no_history_head_label)
emptyView.setMessage(R.string.no_history_label)
return root
}
override fun createListAdaptor() {
listAdapter = object : EpisodeItemListAdapter(activity as MainActivity) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeItemViewHolder {
return object: EpisodeItemViewHolder(mainActivityRef.get()!!, parent) {
override fun setPubDate(item: FeedItem) {
val playDate = Date(item.media?.getLastPlayedTime()?:0L)
pubDate.text = DateFormatter.formatAbbrev(activity, playDate)
pubDate.setContentDescription(DateFormatter.formatForAccessibility(playDate))
}
}
}
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
super.onCreateContextMenu(menu, v, menuInfo)
MenuItemUtils.setOnClickListeners(menu) { item: MenuItem -> 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<FeedItem> {
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<FeedItem> {
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"
}

View File

@ -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)

View File

@ -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) {

View File

@ -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())

View File

@ -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<FeedItem> = 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()

View File

@ -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<NavDrawerData.FeedDrawerItem> = mutableListOf()
private var feedListFiltered: List<NavDrawerData.FeedDrawerItem> = 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

View File

@ -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

View File

@ -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) {

View File

@ -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 {

View File

@ -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 {

View File

@ -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<String>, Array<Long>>
protected val filterDatesTo: Pair<Array<String>, Array<Long>>
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<String>, Array<Long>> {
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<String>()
val timestamps = ArrayList<Long>()
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<String>(), timestamps.toTypedArray<Long>())
}
}

View File

@ -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<String>, Array<Long>>
private val filterDatesTo: Pair<Array<String>, Array<Long>>
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<String>, Array<Long>> {
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<String>()
val timestamps = ArrayList<Long>()
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<String>(), timestamps.toTypedArray<Long>())
}
}

View File

@ -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 {

View File

@ -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<MonthlyStatisticsItem> ->
// disposable = Observable.fromCallable { DBReader.getMonthlyTimeStatistics() }
// .subscribeOn(Schedulers.io())
// .observeOn(AndroidSchedulers.mainThread())
// .subscribe({ result: List<MonthlyStatisticsItem> ->
// 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<MonthlyStatisticsItem> = 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 {

View File

@ -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)

View File

@ -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)

View File

@ -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())
}
/**

View File

@ -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)
}

View File

@ -0,0 +1,12 @@
package ac.mdiq.podcini.util.comparator
import ac.mdiq.podcini.storage.model.feed.FeedItem
class PlaybackLastPlayedDateComparator : Comparator<FeedItem> {
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
}
}

View File

@ -1,3 +0,0 @@
package ac.mdiq.podcini.util.event
class DiscoveryDefaultUpdateEvent

View File

@ -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()
}
}
}

View File

@ -1,8 +0,0 @@
package ac.mdiq.podcini.util.event
import ac.mdiq.podcini.storage.model.download.DownloadStatus
class EpisodeDownloadEvent(private val map: Map<String, DownloadStatus>) {
val urls: Set<String>
get() = map.keys
}

View File

@ -1,4 +0,0 @@
package ac.mdiq.podcini.util.event
//TODO: need to specify ids
class FavoritesEvent

View File

@ -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<FeedItem>) {
companion object {
fun updated(items: List<FeedItem>): FeedItemEvent {
return FeedItemEvent(items)
}
@JvmStatic
fun updated(vararg items: FeedItem): FeedItemEvent {
return FeedItemEvent(listOf(*items))
}
}
}

View File

@ -1,25 +0,0 @@
package ac.mdiq.podcini.util.event
import ac.mdiq.podcini.storage.model.feed.Feed
class FeedListUpdateEvent {
private val feeds: MutableList<Long> = ArrayList()
constructor(feeds: List<Feed>) {
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)
}
}

Some files were not shown because too many files have changed in this diff Show More