diff --git a/app/build.gradle b/app/build.gradle
index 2f828647..d5fb2968 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- versionCode 3020231
- versionName "6.3.7"
+ versionCode 3020232
+ versionName "6.4.0"
applicationId "ac.mdiq.podcini.R"
def commit = ""
@@ -125,6 +125,15 @@ android {
}
}
+ splits {
+ abi {
+ enable true
+ reset()
+ include "arm64-v8a" // Specify the ABI you want to split.
+ universalApk true // This will generate a universal APK that includes all ABIs.
+ }
+ }
+
buildTypes {
release {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg"
@@ -156,7 +165,7 @@ android {
}
dependencies {
- def composeBom = platform('androidx.compose:compose-bom:2024.06.00')
+ def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
implementation composeBom
androidTestImplementation composeBom
@@ -215,6 +224,7 @@ dependencies {
// implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
+// 5.5.0-b01 is newer than 5.5.0-compose01
implementation 'com.mikepenz:iconics-core:5.5.0-b01'
implementation 'com.mikepenz:iconics-views:5.5.0-b01'
implementation 'com.mikepenz:google-material-typeface:4.0.0.3-kotlin@aar'
@@ -243,7 +253,7 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1"
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.6.1"
androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1"
- androidTestImplementation "androidx.test:runner:1.6.1"
+ androidTestImplementation "androidx.test:runner:1.6.2"
androidTestImplementation "androidx.test:rules:1.6.1"
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'org.awaitility:awaitility:4.2.1'
diff --git a/app/src/androidTest/kotlin/ac/test/podcini/playback/PlaybackTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/playback/PlaybackTest.kt
index 244936a9..afe00e9b 100644
--- a/app/src/androidTest/kotlin/ac/test/podcini/playback/PlaybackTest.kt
+++ b/app/src/androidTest/kotlin/ac/test/podcini/playback/PlaybackTest.kt
@@ -1,7 +1,7 @@
package de.test.podcini.playback
import ac.mdiq.podcini.R
-import ac.mdiq.podcini.playback.PlaybackController
+import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
@@ -46,7 +46,7 @@ class PlaybackTest {
private var uiTestUtils: UITestUtils? = null
protected lateinit var context: Context
- private var controller: PlaybackController? = null
+ private var controller: ServiceStatusHandler? = null
@Before
@Throws(Exception::class)
@@ -71,7 +71,7 @@ class PlaybackTest {
}
private fun setupPlaybackController() {
- controller = object : PlaybackController(activityTestRule.activity) {
+ controller = object : ServiceStatusHandler(activityTestRule.activity) {
override fun loadMediaInfo() {
// Do nothing
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index e2488ccf..dd65545c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -56,7 +56,7 @@
tools:ignore="ExportedService">
-
+
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackController.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackController.kt
deleted file mode 100644
index 7e0ad7ec..00000000
--- a/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackController.kt
+++ /dev/null
@@ -1,394 +0,0 @@
-package ac.mdiq.podcini.playback
-
-import ac.mdiq.podcini.playback.base.InTheatre.curMedia
-import ac.mdiq.podcini.playback.base.InTheatre.curState
-import ac.mdiq.podcini.playback.base.MediaPlayerBase
-import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
-import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
-import ac.mdiq.podcini.playback.base.PlayerStatus
-import ac.mdiq.podcini.playback.service.PlaybackService
-import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
-import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
-import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isRunning
-import ac.mdiq.podcini.playback.service.PlaybackService.LocalBinder
-import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
-import ac.mdiq.podcini.storage.model.Playable
-import ac.mdiq.podcini.storage.model.MediaType
-import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
-import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
-import ac.mdiq.podcini.util.Logd
-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
-import android.util.Log
-import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.lifecycleScope
-import androidx.media3.common.util.UnstableApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-
-/**
- * Communicates with the playback service. GUI classes should use this class to
- * control playback instead of communicating with the PlaybackService directly.
- */
-@UnstableApi
-abstract class PlaybackController(private val activity: FragmentActivity) {
-
- private var mediaInfoLoaded = false
- private var loadedFeedMediaId: Long = -1
- private var released = false
- private var initialized = false
- private var eventsRegistered = false
-
- private val mConnection: ServiceConnection = object : ServiceConnection {
- override fun onServiceConnected(className: ComponentName, service: IBinder) {
- if (service is LocalBinder) {
- playbackService = service.service
- onPlaybackServiceConnected()
- if (!released) {
- queryService()
- Logd(TAG, "Connection to Service established")
- } else Logd(TAG, "Connection to playback service has been established, but controller has already been released")
- }
- }
-
- override fun onServiceDisconnected(name: ComponentName) {
- playbackService = null
- initialized = false
- Logd(TAG, "Disconnected from Service")
- }
- }
-
- private var prevStatus = PlayerStatus.STOPPED
- private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- Log.d(TAG, "statusUpdate onReceive called with action: ${intent.action}")
- if (playbackService != null && mPlayerInfo != null) {
- val info = mPlayerInfo!!
- Logd(TAG, "statusUpdate onReceive $prevStatus ${MediaPlayerBase.status} ${info.playerStatus} ${curMedia?.getIdentifier()} ${info.playable?.getIdentifier()}.")
- if (prevStatus != info.playerStatus || curMedia == null || curMedia!!.getIdentifier() != info.playable?.getIdentifier()) {
- MediaPlayerBase.status = info.playerStatus
- prevStatus = MediaPlayerBase.status
- curMedia = info.playable
- handleStatus()
- }
- } else {
- Logd(TAG, "statusUpdate onReceive: Couldn't receive status update: playbackService was null")
- if (isRunning) bindToService()
- else {
- MediaPlayerBase.status = PlayerStatus.STOPPED
- handleStatus()
- }
- }
- }
- }
-
- private val notificationReceiver: BroadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- Log.d(TAG, "notificationReceiver onReceive called with action: ${intent.action}")
- val type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1)
- val code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1)
- if (code == -1 || type == -1) {
- Logd(TAG, "Bad arguments. Won't handle intent")
- return
- }
- when (type) {
- PlaybackService.NOTIFICATION_TYPE_RELOAD -> {
- if (playbackService == null && isRunning) {
- bindToService()
- return
- }
- mediaInfoLoaded = false
- queryService()
- }
- PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END -> onPlaybackEnd()
- }
- }
- }
-
- @Synchronized
- fun init() {
- Logd(TAG, "controller init")
- if (!eventsRegistered) {
- procFlowEvents()
- eventsRegistered = true
- }
- if (isRunning) initServiceRunning()
- else updatePlayButtonShowsPlay(true)
- }
-
- private var eventSink: Job? = null
- private fun cancelFlowEvents() {
- eventSink?.cancel()
- eventSink = null
- }
- private fun procFlowEvents() {
- if (eventSink != null) return
- eventSink = activity.lifecycleScope.launch {
- EventFlow.events.collectLatest { event ->
- Logd(TAG, "Received event: ${event.TAG}")
- when (event) {
- is FlowEvent.PlaybackServiceEvent -> if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_STARTED) init()
- else -> {}
- }
- }
- }
- }
-
- @Synchronized
- private fun initServiceRunning() {
- if (initialized) return
- initialized = true
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED), Context.RECEIVER_NOT_EXPORTED)
- activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackService.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED)
- } else {
- activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED))
- activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackService.ACTION_PLAYER_NOTIFICATION))
- }
-
-// TODO: java.lang.IllegalStateException: Can't call init() after release() has been called
-// at ac.mdiq.podcini.playback.PlaybackController.initServiceRunning(SourceFile:104)
- if (!released) {
- bindToService()
- } else {
- released = false
- bindToService()
- Logd(TAG, "Testing bindToService if released")
-// throw IllegalStateException("Can't call init() after release() has been called")
- }
- checkMediaInfoLoaded()
- }
-
- /**
- * Should be called if the PlaybackController is no longer needed, for
- * example in the activity's onStop() method.
- */
- fun release() {
- Logd(TAG, "Releasing PlaybackController")
- try {
- activity.unregisterReceiver(statusUpdate)
- activity.unregisterReceiver(notificationReceiver)
- } catch (e: IllegalArgumentException) {
- // ignore
- }
- unbind()
- cancelFlowEvents()
- released = true
- eventsRegistered = false
- }
-
- private fun unbind() {
- try { activity.unbindService(mConnection) } catch (e: IllegalArgumentException) { }
- initialized = false
- }
-
- /**
- * Should be called in the activity's onPause() method.
- */
- fun pause() {
-// TODO: why set it to false
-// mediaInfoLoaded = false
- Logd(TAG, "pause() does nothing")
- }
-
- /**
- * Tries to establish a connection to the PlaybackService. If it isn't
- * running, the PlaybackService will be started with the last played media
- * as the arguments of the launch intent.
- */
- private fun bindToService() {
- Logd(TAG, "Trying to connect to service")
- check(isRunning) { "Trying to bind but service is not running" }
- val bound = activity.bindService(Intent(activity, PlaybackService::class.java), mConnection, 0)
- Logd(TAG, "Result for service binding: $bound")
- }
-
- open fun onPlaybackEnd() {}
-
- /**
- * Is called whenever the PlaybackService changes its status. This method
- * should be used to update the GUI or start/cancel background threads.
- */
- private fun handleStatus() {
- Log.d(TAG, "handleStatus() called status: ${MediaPlayerBase.status}")
- checkMediaInfoLoaded()
-
- when (MediaPlayerBase.status) {
- PlayerStatus.PLAYING -> updatePlayButtonShowsPlay(false)
- PlayerStatus.PREPARING -> updatePlayButtonShowsPlay(!isStartWhenPrepared)
- PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED ->
- updatePlayButtonShowsPlay(true)
- else -> {}
- }
- }
-
- private fun checkMediaInfoLoaded() {
- if (!mediaInfoLoaded || loadedFeedMediaId != curState.curMediaId) {
- loadedFeedMediaId = curState.curMediaId
- Logd(TAG, "checkMediaInfoLoaded: $loadedFeedMediaId")
- loadMediaInfo()
- }
- mediaInfoLoaded = true
- }
-
- protected open fun updatePlayButtonShowsPlay(showPlay: Boolean) {}
-
- abstract fun loadMediaInfo()
-
- open fun onPlaybackServiceConnected() { }
-
- /**
- * Called when connection to playback service has been established or
- * information has to be refreshed
- */
- private fun queryService() {
- Logd(TAG, "Querying service info")
- if (playbackService != null && mPlayerInfo != null) {
- MediaPlayerBase.status = mPlayerInfo!!.playerStatus
- curMedia = mPlayerInfo!!.playable
- // make sure that new media is loaded if it's available
- mediaInfoLoaded = false
- handleStatus()
- } else {
- Log.e(TAG, "queryService() was called without an existing connection to playbackservice")
- }
- }
-
- fun ensureService() {
- if (curMedia == null) return
- if (playbackService == null) {
- PlaybackServiceStarter(activity, curMedia!!).start()
-// Log.w(TAG, "playbackservice was null, restarted!")
- }
- }
-
- fun playPause() {
- if (curMedia == null) return
- if (playbackService == null) {
- PlaybackServiceStarter(activity, curMedia!!).start()
- Logd(TAG, "playbackservice was null, restarted!")
- return
- }
- when (MediaPlayerBase.status) {
- PlayerStatus.FALLBACK -> fallbackSpeed(1.0f)
- PlayerStatus.PLAYING -> {
- playbackService?.mPlayer?.pause(true, reinit = false)
- playbackService?.isSpeedForward = false
- playbackService?.isFallbackSpeed = false
-// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
- }
- PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
- playbackService?.mPlayer?.resume()
- playbackService?.taskManager?.restartSleepTimer()
-// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!))
- }
- PlayerStatus.PREPARING -> isStartWhenPrepared = !isStartWhenPrepared
- PlayerStatus.INITIALIZED -> {
- if (playbackService != null) isStartWhenPrepared = true
- playbackService?.mPlayer?.prepare()
- playbackService?.taskManager?.restartSleepTimer()
- }
- else -> {
- PlaybackServiceStarter(activity, curMedia!!).callEvenIfRunning(true).start()
- Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown")
- }
- }
- }
-
- companion object {
- private val TAG: String = PlaybackController::class.simpleName ?: "Anonymous"
-
- var playbackService: PlaybackService? = null
-
- val curPosition: Int
- get() = playbackService?.curPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
-
- val duration: Int
- get() = playbackService?.curDuration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
-
- val curSpeedMultiplier: Float
- get() = playbackService?.curSpeed ?: getCurrentPlaybackSpeed(curMedia)
-
- val isPlayingVideoLocally: Boolean
- get() = when {
- isCasting -> false
- playbackService != null -> currentMediaType == MediaType.VIDEO
- else -> curMedia?.getMediaType() == MediaType.VIDEO
- }
-
- private var isStartWhenPrepared: Boolean
- get() = playbackService?.mPlayer?.startWhenPrepared?.get() ?: false
- set(s) {
- playbackService?.mPlayer?.startWhenPrepared?.set(s)
- }
-
- private val mPlayerInfo: MediaPlayerInfo?
- get() = playbackService?.mPlayer?.playerInfo
-
- fun seekTo(time: Int) {
- if (playbackService != null) {
- playbackService!!.mPlayer?.seekTo(time)
-// if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(curMedia, time, duration))
- }
- }
-
- fun fallbackSpeed(speed: Float) {
- if (playbackService != null) {
- when (MediaPlayerBase.status) {
- PlayerStatus.PLAYING -> {
- MediaPlayerBase.status = PlayerStatus.FALLBACK
- setToFallback(speed)
- }
- PlayerStatus.FALLBACK -> {
- MediaPlayerBase.status = PlayerStatus.PLAYING
- setToFallback(speed)
- }
- else -> {}
- }
- }
- }
-
- private fun setToFallback(speed: Float) {
- if (playbackService?.mPlayer == null || playbackService!!.isSpeedForward) return
-
- if (!playbackService!!.isFallbackSpeed) {
- playbackService!!.normalSpeed = playbackService!!.mPlayer!!.getPlaybackSpeed()
- playbackService!!.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
- } else playbackService!!.mPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
-
- playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
- }
-
- fun sleepTimerActive(): Boolean {
- return playbackService?.taskManager?.isSleepTimerActive ?: false
- }
-
- /**
- * Returns an intent which starts an audio- or videoplayer, depending on the
- * type of media that is being played. If the playbackservice is not
- * running, the type of the last played media will be looked up.
- */
- @JvmStatic
- fun getPlayerActivityIntent(context: Context): Intent {
- val showVideoPlayer = if (isRunning) currentMediaType == MediaType.VIDEO && !isCasting
- else curState.curIsVideo
- return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent
- else MainActivityStarter(context).withOpenPlayer().getIntent()
- }
-
- /**
- * Same as [.getPlayerActivityIntent], but here the type of activity
- * depends on the medaitype that is provided as an argument.
- */
- @JvmStatic
- fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent {
- return if (mediaType == MediaType.VIDEO && !isCasting) VideoPlayerActivityStarter(context).intent
- else MainActivityStarter(context).withOpenPlayer().getIntent()
- }
- }
-}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/ServiceStatusHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/ServiceStatusHandler.kt
new file mode 100644
index 00000000..da59e16a
--- /dev/null
+++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/ServiceStatusHandler.kt
@@ -0,0 +1,220 @@
+package ac.mdiq.podcini.playback
+
+import ac.mdiq.podcini.playback.base.InTheatre.curMedia
+import ac.mdiq.podcini.playback.base.InTheatre.curState
+import ac.mdiq.podcini.playback.base.MediaPlayerBase
+import ac.mdiq.podcini.playback.base.PlayerStatus
+import ac.mdiq.podcini.playback.service.PlaybackService
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isRunning
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
+import ac.mdiq.podcini.storage.model.MediaType
+import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
+import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
+import ac.mdiq.podcini.util.Logd
+import ac.mdiq.podcini.util.event.EventFlow
+import ac.mdiq.podcini.util.event.FlowEvent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Build
+import android.util.Log
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import androidx.media3.common.util.UnstableApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+
+/**
+ * Communicates with the playback service. GUI classes should use this class to
+ * control playback instead of communicating with the PlaybackService directly.
+ */
+@UnstableApi
+abstract class ServiceStatusHandler(private val activity: FragmentActivity) {
+
+ private var mediaInfoLoaded = false
+ private var loadedFeedMediaId: Long = -1
+ private var initialized = false
+
+ private var prevStatus = PlayerStatus.STOPPED
+ private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "statusUpdate onReceive called with action: ${intent.action}")
+ if (playbackService != null && PlaybackService.mPlayerInfo != null) {
+ val info = PlaybackService.mPlayerInfo!!
+// Logd(TAG, "statusUpdate onReceive $prevStatus ${MediaPlayerBase.status} ${info.playerStatus} ${curMedia?.getIdentifier()} ${info.playable?.getIdentifier()}.")
+ if (prevStatus != info.playerStatus || curMedia == null || curMedia!!.getIdentifier() != info.playable?.getIdentifier()) {
+ Logd(TAG, "statusUpdate onReceive doing updates")
+ MediaPlayerBase.status = info.playerStatus
+ prevStatus = MediaPlayerBase.status
+ curMedia = info.playable
+ handleStatus()
+ }
+ } else {
+ Logd(TAG, "statusUpdate onReceive: Couldn't receive status update: playbackService was null")
+ if (!isRunning) {
+ MediaPlayerBase.status = PlayerStatus.STOPPED
+ handleStatus()
+ }
+ }
+ }
+ }
+
+ private val notificationReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ Log.d(TAG, "notificationReceiver onReceive called with action: ${intent.action}")
+ val type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1)
+ val code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1)
+ if (code == -1 || type == -1) {
+ Logd(TAG, "Bad arguments. Won't handle intent")
+ return
+ }
+ when (type) {
+ PlaybackService.NOTIFICATION_TYPE_RELOAD -> {
+ if (playbackService == null && isRunning) return
+ mediaInfoLoaded = false
+ updateStatus()
+ }
+ PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END -> onPlaybackEnd()
+ }
+ }
+ }
+
+ @Synchronized
+ fun init() {
+ Logd(TAG, "controller init")
+ procFlowEvents()
+ if (isRunning) initServiceRunning()
+ else updatePlayButton(true)
+ }
+
+ private var eventSink: Job? = null
+ private fun cancelFlowEvents() {
+ eventSink?.cancel()
+ eventSink = null
+ }
+ private fun procFlowEvents() {
+ if (eventSink != null) return
+ eventSink = activity.lifecycleScope.launch {
+ EventFlow.events.collectLatest { event ->
+ Logd(TAG, "Received event: ${event.TAG}")
+ when (event) {
+ is FlowEvent.PlaybackServiceEvent -> {
+ if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_STARTED) {
+ init()
+ updateStatus()
+ }
+ }
+ else -> {}
+ }
+ }
+ }
+ }
+
+ @Synchronized
+ private fun initServiceRunning() {
+ if (initialized) return
+ initialized = true
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED), Context.RECEIVER_NOT_EXPORTED)
+ activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackService.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED)
+ } else {
+ activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED))
+ activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackService.ACTION_PLAYER_NOTIFICATION))
+ }
+ checkMediaInfoLoaded()
+ }
+
+ /**
+ * Should be called if the PlaybackController is no longer needed, for
+ * example in the activity's onStop() method.
+ */
+ fun release() {
+ Logd(TAG, "Releasing PlaybackController")
+ try {
+ activity.unregisterReceiver(statusUpdate)
+ activity.unregisterReceiver(notificationReceiver)
+ } catch (e: IllegalArgumentException) {
+ // ignore
+ }
+ initialized = false
+ cancelFlowEvents()
+ }
+
+ open fun onPlaybackEnd() {}
+
+ /**
+ * Is called whenever the PlaybackService changes its status. This method
+ * should be used to update the GUI or start/cancel background threads.
+ */
+ private fun handleStatus() {
+ Log.d(TAG, "handleStatus() called status: ${MediaPlayerBase.status}")
+ checkMediaInfoLoaded()
+ when (MediaPlayerBase.status) {
+ PlayerStatus.PLAYING -> updatePlayButton(false)
+ PlayerStatus.PREPARING -> updatePlayButton(!PlaybackService.isStartWhenPrepared)
+ PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED ->
+ updatePlayButton(true)
+ else -> {}
+ }
+ }
+
+ private fun checkMediaInfoLoaded() {
+ if (!mediaInfoLoaded || loadedFeedMediaId != curState.curMediaId) {
+ loadedFeedMediaId = curState.curMediaId
+ Logd(TAG, "checkMediaInfoLoaded: $loadedFeedMediaId")
+ loadMediaInfo()
+ }
+ mediaInfoLoaded = true
+ }
+
+ protected open fun updatePlayButton(showPlay: Boolean) {}
+
+ abstract fun loadMediaInfo()
+
+ /**
+ * Called when connection to playback service has been established or
+ * information has to be refreshed
+ */
+ private fun updateStatus() {
+ Logd(TAG, "Querying service info")
+ if (playbackService != null && PlaybackService.mPlayerInfo != null) {
+ MediaPlayerBase.status = PlaybackService.mPlayerInfo!!.playerStatus
+ curMedia = PlaybackService.mPlayerInfo!!.playable
+ // make sure that new media is loaded if it's available
+ mediaInfoLoaded = false
+ handleStatus()
+ } else Log.e(TAG, "queryService() was called without an existing connection to playbackservice")
+ }
+
+ companion object {
+ private val TAG: String = ServiceStatusHandler::class.simpleName ?: "Anonymous"
+
+ /**
+ * Returns an intent which starts an audio- or videoplayer, depending on the
+ * type of media that is being played. If the playbackservice is not
+ * running, the type of the last played media will be looked up.
+ */
+ @JvmStatic
+ fun getPlayerActivityIntent(context: Context): Intent {
+ val showVideoPlayer = if (isRunning) currentMediaType == MediaType.VIDEO && !isCasting
+ else curState.curIsVideo
+ return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent
+ else MainActivityStarter(context).withOpenPlayer().getIntent()
+ }
+
+ /**
+ * Same as [.getPlayerActivityIntent], but here the type of activity
+ * depends on the medaitype that is provided as an argument.
+ */
+ @JvmStatic
+ fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent {
+ return if (mediaType == MediaType.VIDEO && !isCasting) VideoPlayerActivityStarter(context).intent
+ else MainActivityStarter(context).withOpenPlayer().getIntent()
+ }
+ }
+}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt
index 5c6d466d..05290a47 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt
@@ -3,22 +3,23 @@ package ac.mdiq.podcini.playback.base
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.preferences.UserPreferences
-import ac.mdiq.podcini.preferences.UserPreferences.Prefs.prefPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.FeedPreferences
-import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.MediaType
-import ac.mdiq.podcini.util.Logd
+import ac.mdiq.podcini.storage.model.Playable
import android.content.Context
import android.media.AudioManager
+import android.net.Uri
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock
import android.util.Log
import android.util.Pair
import android.view.SurfaceHolder
+import androidx.media3.common.MediaItem
+import androidx.media3.common.MediaMetadata
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.Volatile
@@ -316,6 +317,28 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
}
}
+ fun buildMetadata(p: Playable): MediaMetadata {
+ val builder = MediaMetadata.Builder()
+ .setIsBrowsable(true)
+ .setIsPlayable(true)
+ .setArtist(p.getFeedTitle())
+ .setTitle(p.getEpisodeTitle())
+ .setAlbumArtist(p.getFeedTitle())
+ .setDisplayTitle(p.getEpisodeTitle())
+ .setSubtitle(p.getFeedTitle())
+ .setArtworkUri(null)
+ return builder.build()
+ }
+
+ fun buildMediaItem(p: Playable): MediaItem? {
+ val url = p.getStreamUrl() ?: return null
+ val metadata = buildMetadata(p)
+ return MediaItem.Builder()
+ .setMediaId(url)
+ .setUri(Uri.parse(url))
+ .setMediaMetadata(metadata).build()
+ }
+
/**
* @param currentPosition current position in a media file in ms
* @param lastPlayedTime timestamp when was media paused
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt
index 59110b11..e23536dd 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt
@@ -166,17 +166,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
exoPlayer?.setAudioAttributes(b.build(), true)
}
- private fun buildMetadata(p: Playable): MediaMetadata {
- val builder = MediaMetadata.Builder()
- .setArtist(p.getFeedTitle())
- .setTitle(p.getEpisodeTitle())
- .setAlbumArtist(p.getFeedTitle())
- .setDisplayTitle(p.getEpisodeTitle())
- .setSubtitle(p.getFeedTitle())
- .setArtworkUri(null)
- return builder.build()
- }
-
@Throws(IllegalArgumentException::class, IllegalStateException::class)
private fun setDataSource(metadata: MediaMetadata, mediaUrl: String, user: String?, password: String?) {
Logd(TAG, "setDataSource: $mediaUrl")
@@ -681,6 +670,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
private var httpDataSourceFactory: OkHttpDataSource.Factory? = null
private var trackSelector: DefaultTrackSelector? = null
+
var exoPlayer: ExoPlayer? = null
private var exoplayerListener: Listener? = null
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt
index 9bd8bfe8..7346a1c1 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt
@@ -14,6 +14,8 @@ import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.playback.base.InTheatre.loadPlayableFromPreferences
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
import ac.mdiq.podcini.playback.base.MediaPlayerBase
+import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.buildMediaItem
+import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.PlayerStatus
@@ -28,6 +30,7 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
+import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
@@ -74,6 +77,7 @@ import android.webkit.URLUtil
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
+import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player.STATE_ENDED
import androidx.media3.common.Player.STATE_IDLE
@@ -93,7 +97,9 @@ import kotlin.math.max
* Controls the MediaPlayer that plays a EpisodeMedia-file
*/
@UnstableApi
-class PlaybackService : MediaSessionService() {
+class PlaybackService : MediaLibraryService() {
+
+ private var mediaSession: MediaLibrarySession? = null
internal var mPlayer: MediaPlayerBase? = null
internal lateinit var taskManager: TaskManager
@@ -110,7 +116,6 @@ class PlaybackService : MediaSessionService() {
private var autoSkippedFeedMediaId: String? = null
internal var normalSpeed = 1.0f
- private var mediaSession: MediaSession? = null
private val mBinder: IBinder = LocalBinder()
private var clickCount = 0
@@ -475,20 +480,162 @@ class PlaybackService : MediaSessionService() {
}
}
- inner class LocalBinder : Binder() {
- val service: PlaybackService
- get() = this@PlaybackService
+ val rootItem = MediaItem.Builder()
+ .setMediaId("CurQueue")
+ .setMediaMetadata(
+ MediaMetadata.Builder()
+ .setIsBrowsable(true)
+ .setIsPlayable(false)
+ .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
+ .setTitle(curQueue.name)
+ .build())
+ .build()
+
+ val mediaItemsInQueue: MutableList by lazy {
+ val list = mutableListOf()
+ curQueue.episodes.forEach {
+ if (it.media != null) {
+ val item = buildMediaItem(it.media!!)
+ if (item != null) list += item
+ }
+ }
+ Logd(TAG, "mediaItemsInQueue: ${list.size}")
+ list
}
- override fun onUnbind(intent: Intent): Boolean {
- Logd(TAG, "Received onUnbind event")
- return super.onUnbind(intent)
+ val mediaLibrarySessionCK = object: MediaLibrarySession.Callback {
+ override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
+ Logd(TAG, "in MyMediaSessionCallback onConnect")
+ when {
+ session.isMediaNotificationController(controller) -> {
+ Logd(TAG, "MyMediaSessionCallback onConnect isMediaNotificationController")
+ val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
+ val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
+ notificationCustomButtons.forEach { commandButton ->
+ Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
+ commandButton.sessionCommand?.let(sessionCommands::add)
+ }
+ return MediaSession.ConnectionResult.accept(sessionCommands.build(), playerCommands.build())
+ }
+ session.isAutoCompanionController(controller) -> {
+ Logd(TAG, "MyMediaSessionCallback onConnect isAutoCompanionController")
+ val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
+ notificationCustomButtons.forEach { commandButton ->
+ Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
+ commandButton.sessionCommand?.let(sessionCommands::add)
+ }
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
+ .setAvailableSessionCommands(sessionCommands.build())
+ .build()
+ }
+ else -> {
+ Logd(TAG, "MyMediaSessionCallback onConnect other controller: ${controller.toString()}")
+ return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
+ }
+ }
+ }
+ override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
+ super.onPostConnect(session, controller)
+ Logd(TAG, "MyMediaSessionCallback onPostConnect")
+ if (notificationCustomButtons.isNotEmpty()) {
+ mediaSession?.setCustomLayout(notificationCustomButtons)
+// mediaSession?.setCustomLayout(customMediaNotificationProvider.notificationMediaButtons)
+ }
+ }
+ override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture {
+ Log.d(TAG, "MyMediaSessionCallback onCustomCommand ${customCommand.customAction}")
+ /* Handling custom command buttons from player notification. */
+ when (customCommand.customAction) {
+ NotificationCustomButton.REWIND.customAction -> mPlayer?.seekDelta(-rewindSecs * 1000)
+ NotificationCustomButton.FORWARD.customAction -> mPlayer?.seekDelta(fastForwardSecs * 1000)
+ NotificationCustomButton.SKIP.customAction -> mPlayer?.skip()
+ }
+ return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
+ }
+ override fun onPlaybackResumption(mediaSession: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture {
+ Logd(TAG, "MyMediaSessionCallback onPlaybackResumption ")
+ val settable = SettableFuture.create()
+// scope.launch {
+// // Your app is responsible for storing the playlist and the start position
+// // to use here
+// val resumptionPlaylist = restorePlaylist()
+// settable.set(resumptionPlaylist)
+// }
+ return settable
+ }
+ override fun onDisconnected(session: MediaSession, controller: MediaSession.ControllerInfo) {
+ Logd(TAG, "in MyMediaSessionCallback onDisconnected")
+ when {
+ session.isMediaNotificationController(controller) -> {
+ Logd(TAG, "MyMediaSessionCallback onDisconnected isMediaNotificationController")
+ }
+ session.isAutoCompanionController(controller) -> {
+ Logd(TAG, "MyMediaSessionCallback onDisconnected isAutoCompanionController")
+ }
+ }
+ }
+ override fun onMediaButtonEvent(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, intent: Intent): Boolean {
+ val keyEvent = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) intent.extras!!.getParcelable(EXTRA_KEY_EVENT, KeyEvent::class.java)
+ else intent.extras!!.getParcelable(EXTRA_KEY_EVENT) as? KeyEvent
+ Log.d(TAG, "onMediaButtonEvent ${keyEvent?.keyCode}")
+
+ if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent.repeatCount == 0) {
+ val keyCode = keyEvent.keyCode
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+ clickCount++
+ clickHandler.removeCallbacksAndMessages(null)
+ clickHandler.postDelayed({
+ when (clickCount) {
+ 1 -> handleKeycode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false)
+ 2 -> mPlayer?.seekDelta(fastForwardSecs * 1000)
+ 3 -> mPlayer?.seekDelta(-rewindSecs * 1000)
+ }
+ clickCount = 0
+ }, ViewConfiguration.getDoubleTapTimeout().toLong())
+ return true
+ } else return handleKeycode(keyCode, false)
+ }
+ return false
+ }
+ override fun onGetItem(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, mediaId: String): ListenableFuture> {
+ Logd(TAG, "MyMediaSessionCallback onGetItem called mediaId:$mediaId")
+ return super.onGetItem(session, browser, mediaId)
+ }
+ override fun onGetLibraryRoot(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, params: LibraryParams?): ListenableFuture> {
+ Logd(TAG, "MyMediaSessionCallback onGetLibraryRoot called")
+ return Futures.immediateFuture(LibraryResult.ofItem(rootItem, params))
+ }
+ override fun onGetChildren(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, parentId: String, page: Int, pageSize: Int,
+ params: LibraryParams?): ListenableFuture>> {
+ Logd(TAG, "MyMediaSessionCallback onGetChildren called parentId:$parentId page:$page pageSize:$pageSize")
+// return super.onGetChildren(session, browser, parentId, page, pageSize, params)
+ return Futures.immediateFuture(LibraryResult.ofItemList(mediaItemsInQueue, params))
+ }
+ override fun onSubscribe(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, parentId: String,
+ params: LibraryParams?): ListenableFuture> {
+ return Futures.immediateFuture(LibraryResult.ofVoid())
+ }
+ override fun onAddMediaItems(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: MutableList): ListenableFuture> {
+ Logd(TAG, "MyMediaSessionCallback onAddMediaItems called ${mediaItems.size} ${mediaItems[0]}")
+ /* This is the trickiest part, if you don't do this here, nothing will play */
+ val episode = getEpisodeByGuidOrUrl(null, mediaItems.first().mediaId) ?: return Futures.immediateFuture(mutableListOf())
+ val media = episode.media ?: return Futures.immediateFuture(mutableListOf())
+ if (!InTheatre.isCurMedia(media)) {
+ PlaybackServiceStarter(applicationContext, media).callEvenIfRunning(true).start()
+ EventFlow.postEvent(FlowEvent.PlayEvent(episode))
+ }
+ val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList()
+// updatedMediaItems += mediaItemsInQueue
+// Logd(TAG, "MyMediaSessionCallback onAddMediaItems updatedMediaItems: ${updatedMediaItems.size} ")
+ return Futures.immediateFuture(updatedMediaItems)
+ }
}
override fun onCreate() {
super.onCreate()
Logd(TAG, "Service created.")
isRunning = true
+ playbackService = this
if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED)
@@ -523,10 +670,10 @@ class PlaybackService : MediaSessionService() {
recreateMediaPlayer()
if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext)
val intent = packageManager.getLaunchIntentForPackage(packageName)
- val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
- mediaSession = MediaSession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!)
+ val pendingIntent = PendingIntent.getActivity(this, 0, intent, if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else FLAG_UPDATE_CURRENT)
+ mediaSession = MediaLibrarySession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!, mediaLibrarySessionCK)
+ .setId(packageName)
.setSessionActivity(pendingIntent)
- .setCallback(MyMediaSessionCallback())
.setCustomLayout(notificationCustomButtons)
.build()
}
@@ -561,6 +708,7 @@ class PlaybackService : MediaSessionService() {
override fun onDestroy() {
Logd(TAG, "Service is about to be destroyed")
+ playbackService = null
isRunning = false
currentMediaType = MediaType.UNKNOWN
castStateListener.destroy()
@@ -591,99 +739,19 @@ class PlaybackService : MediaSessionService() {
return mediaSession?.player?.playbackState != STATE_IDLE && mediaSession?.player?.playbackState != STATE_ENDED
}
- private inner class MyMediaSessionCallback : MediaSession.Callback {
- override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
- Logd(TAG, "in MyMediaSessionCallback onConnect")
- val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
-// .add(NotificationCustomButton.REWIND)
-// .add(NotificationCustomButton.FORWARD)
- when {
- session.isMediaNotificationController(controller) -> {
- Logd(TAG, "MyMediaSessionCallback onConnect isMediaNotificationController")
- val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
- notificationCustomButtons.forEach { commandButton ->
- Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
- commandButton.sessionCommand?.let(sessionCommands::add)
- }
- return MediaSession.ConnectionResult.accept(
- sessionCommands.build(),
- playerCommands.build()
- )
- }
- session.isAutoCompanionController(controller) -> {
- Logd(TAG, "MyMediaSessionCallback onConnect isAutoCompanionController")
- return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
- .setAvailableSessionCommands(sessionCommands.build())
- .build()
- }
- else -> {
- Logd(TAG, "MyMediaSessionCallback onConnect other controller")
- return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
- }
- }
- }
- override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
- super.onPostConnect(session, controller)
- Logd(TAG, "MyMediaSessionCallback onPostConnect")
- if (notificationCustomButtons.isNotEmpty()) {
- mediaSession?.setCustomLayout(notificationCustomButtons)
-// mediaSession?.setCustomLayout(customMediaNotificationProvider.notificationMediaButtons)
- }
- }
- override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture {
- Log.d(TAG, "onCustomCommand ${customCommand.customAction}")
- /* Handling custom command buttons from player notification. */
- when (customCommand.customAction) {
- NotificationCustomButton.REWIND.customAction -> mPlayer?.seekDelta(-rewindSecs * 1000)
- NotificationCustomButton.FORWARD.customAction -> mPlayer?.seekDelta(fastForwardSecs * 1000)
- NotificationCustomButton.SKIP.customAction -> mPlayer?.skip()
- }
- return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
- }
- override fun onPlaybackResumption(mediaSession: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture {
- Logd(TAG, "MyMediaSessionCallback onPlaybackResumption ")
- val settable = SettableFuture.create()
-// scope.launch {
-// // Your app is responsible for storing the playlist and the start position
-// // to use here
-// val resumptionPlaylist = restorePlaylist()
-// settable.set(resumptionPlaylist)
-// }
- return settable
- }
- override fun onMediaButtonEvent(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, intent: Intent): Boolean {
- val keyEvent = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) intent.extras!!.getParcelable(EXTRA_KEY_EVENT, KeyEvent::class.java)
- else intent.extras!!.getParcelable(EXTRA_KEY_EVENT) as? KeyEvent
- Log.d(TAG, "onMediaButtonEvent ${keyEvent?.keyCode}")
-
- if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent.repeatCount == 0) {
- val keyCode = keyEvent.keyCode
- if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
- clickCount++
- clickHandler.removeCallbacksAndMessages(null)
- clickHandler.postDelayed({
- when (clickCount) {
- 1 -> handleKeycode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false)
- 2 -> mPlayer?.seekDelta(fastForwardSecs * 1000)
- 3 -> mPlayer?.seekDelta(-rewindSecs * 1000)
- }
- clickCount = 0
- }, ViewConfiguration.getDoubleTapTimeout().toLong())
- return true
- } else return handleKeycode(keyCode, false)
- }
- return false
- }
- }
-
- override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
+ override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
return mediaSession
}
- override fun onBind(intent: Intent?): IBinder? {
- Logd(TAG, "Received onBind event")
- return if (intent?.action != null && intent.action == SERVICE_INTERFACE) super.onBind(intent) else mBinder
- }
+// override fun onBind(intent: Intent?): IBinder? {
+// Logd(TAG, "Received onBind event")
+// return if (intent?.action != null && intent.action == SERVICE_INTERFACE) super.onBind(intent) else mBinder
+// }
+
+// override fun onUnbind(intent: Intent): Boolean {
+// Logd(TAG, "Received onUnbind event")
+// return super.onUnbind(intent)
+// }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
@@ -718,6 +786,11 @@ class PlaybackService : MediaSessionService() {
val handled = handleKeycode(keycode, !hardwareButton)
return super.onStartCommand(intent, flags, startId)
}
+ keyEvent != null && keyEvent.keyCode != -1 -> {
+ Logd(TAG, "onStartCommand Received button event: ${keyEvent.keyCode}")
+ val handled = handleKeycode(keyEvent.keyCode, !hardwareButton)
+ return super.onStartCommand(intent, flags, startId)
+ }
playable != null -> {
Logd(TAG, "onStartCommand status: $status")
val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false
@@ -964,6 +1037,7 @@ class PlaybackService : MediaSessionService() {
private fun onQueueEvent(event: FlowEvent.QueueEvent) {
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}")
+ notifyCurQueueItemsChanged()
for (e in event.episodes) {
Logd(TAG, "onQueueEvent: ending playback event ${e.title}")
if (e.id == curEpisode?.id) {
@@ -974,6 +1048,12 @@ class PlaybackService : MediaSessionService() {
}
}
+ fun notifyCurQueueItemsChanged(range_: Int = -1) {
+ val range = if (range_ > 0) range_ else curQueue.size()
+ Logd(TAG, "notifyCurQueueItemsChanged curQueue: ${curQueue.id}")
+ mediaSession?.notifyChildrenChanged("CurQueue", range, null)
+ }
+
// private fun onVolumeAdaptionChanged(event: FlowEvent.VolumeAdaptionChangedEvent) {
// if (mPlayer != null) updateVolumeIfNecessary(mPlayer!!, event.feedId, event.volumeAdaptionSetting)
// }
@@ -1130,6 +1210,11 @@ class PlaybackService : MediaSessionService() {
}
}
+ inner class LocalBinder : Binder() {
+ val service: PlaybackService
+ get() = this@PlaybackService
+ }
+
enum class NotificationCustomButton(val customAction: String, val commandButton: CommandButton) {
SKIP(
customAction = CUSTOM_COMMAND_SKIP_ACTION_ID,
@@ -1211,6 +1296,9 @@ class PlaybackService : MediaSessionService() {
private const val EXTRA_CODE_VIDEO: Int = 2
private const val EXTRA_CODE_CAST: Int = 3
+ var playbackService: PlaybackService? = null
+ var mediaBrowser: MediaBrowser? = null
+
@JvmField
var isRunning: Boolean = false
@@ -1259,6 +1347,88 @@ class PlaybackService : MediaSessionService() {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefFollowQueue.name, value).apply()
}
+ val curPositionFB: Int
+ get() = playbackService?.curPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
+
+ val curDurationFB: Int
+ get() = playbackService?.curDuration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
+
+ val curSpeedFB: Float
+ get() = playbackService?.curSpeed ?: getCurrentPlaybackSpeed(curMedia)
+
+ val isPlayingVideoLocally: Boolean
+ get() = when {
+ isCasting -> false
+ playbackService != null -> currentMediaType == MediaType.VIDEO
+ else -> curMedia?.getMediaType() == MediaType.VIDEO
+ }
+
+ var isStartWhenPrepared: Boolean
+ get() = playbackService?.mPlayer?.startWhenPrepared?.get() ?: false
+ set(s) {
+ playbackService?.mPlayer?.startWhenPrepared?.set(s)
+ }
+
+ val mPlayerInfo: MediaPlayerInfo?
+ get() = playbackService?.mPlayer?.playerInfo
+
+ fun seekTo(time: Int) {
+ playbackService?.mPlayer?.seekTo(time)
+ }
+
+ fun toggleFallbackSpeed(speed: Float) {
+ if (playbackService != null) {
+ when (MediaPlayerBase.status) {
+ PlayerStatus.PLAYING -> {
+ MediaPlayerBase.status = PlayerStatus.FALLBACK
+ setToFallbackSpeed(speed)
+ }
+ PlayerStatus.FALLBACK -> {
+ MediaPlayerBase.status = PlayerStatus.PLAYING
+ setToFallbackSpeed(speed)
+ }
+ else -> {}
+ }
+ }
+ }
+
+ private fun setToFallbackSpeed(speed: Float) {
+ if (playbackService?.mPlayer == null || playbackService!!.isSpeedForward) return
+
+ if (!playbackService!!.isFallbackSpeed) {
+ playbackService!!.normalSpeed = playbackService!!.mPlayer!!.getPlaybackSpeed()
+ playbackService!!.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
+ } else playbackService!!.mPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
+
+ playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
+ }
+
+ fun isSleepTimerActive(): Boolean {
+ return playbackService?.taskManager?.isSleepTimerActive ?: false
+ }
+
+ fun playPause() {
+ when (MediaPlayerBase.status) {
+ PlayerStatus.FALLBACK -> toggleFallbackSpeed(1.0f)
+ PlayerStatus.PLAYING -> {
+ playbackService?.mPlayer?.pause(true, reinit = false)
+ playbackService?.isSpeedForward = false
+ playbackService?.isFallbackSpeed = false
+ }
+ PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
+ playbackService?.mPlayer?.resume()
+ playbackService?.taskManager?.restartSleepTimer()
+ }
+ PlayerStatus.PREPARING -> isStartWhenPrepared = !isStartWhenPrepared
+ PlayerStatus.INITIALIZED -> {
+ if (playbackService != null) isStartWhenPrepared = true
+ playbackService?.mPlayer?.prepare()
+ playbackService?.taskManager?.restartSleepTimer()
+ }
+ else -> Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown")
+ }
+ }
+
fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
val playable = curMedia
if (playable is EpisodeMedia) {
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt
index 8390e8a2..2cddac1d 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt
@@ -1,17 +1,15 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
-import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
-import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
-import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
+import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.MediaType
-import ac.mdiq.podcini.ui.actions.actionbutton.StreamActionButton.StreamingConfirmationDialog
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt
index ecd7f91a..160ba26a 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt
@@ -1,10 +1,10 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
-import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
-import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
+import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.util.Logd
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt
index 79c2f9fd..6b126bda 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt
@@ -10,7 +10,7 @@ import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.RemoteMedia
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
-import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
+import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.content.Context
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt
index 36cf7e35..d8d85685 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt
@@ -11,7 +11,6 @@ import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
-import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.preferences.ThemeSwitcher.getNoTitleTheme
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer
@@ -36,7 +35,6 @@ 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
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@@ -65,8 +63,6 @@ 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
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
@@ -75,8 +71,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
-import com.google.common.util.concurrent.ListenableFuture
-import com.google.common.util.concurrent.MoreExecutors
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import org.apache.commons.lang3.ArrayUtils
@@ -96,7 +90,7 @@ class MainActivity : CastEnabledActivity() {
private lateinit var navDrawerFragment: NavDrawerFragment
private lateinit var audioPlayerFragment: AudioPlayerFragment
private lateinit var audioPlayerView: View
- private lateinit var controllerFuture: ListenableFuture
+// private lateinit var controllerFuture: ListenableFuture
private lateinit var navDrawer: View
private lateinit var dummyView : View
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
@@ -529,19 +523,20 @@ class MainActivity : CastEnabledActivity() {
procFlowEvents()
RatingDialog.init(this)
- val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
- controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
- controllerFuture.addListener({
- // Call controllerFuture.get() to retrieve the MediaController.
- // MediaController implements the Player interface, so it can be
- // attached to the PlayerView UI component.
-// playerView.setPlayer(controllerFuture.get())
- }, MoreExecutors.directExecutor())
+// val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
+// controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
+// controllerFuture.addListener({
+// // Call controllerFuture.get() to retrieve the MediaController.
+// // MediaController implements the Player interface, so it can be
+// // attached to the PlayerView UI component.
+//// playerView.setPlayer(controllerFuture.get())
+// val player = controllerFuture.get()
+// }, MoreExecutors.directExecutor())
}
override fun onStop() {
super.onStop()
- MediaController.releaseFuture(controllerFuture)
+// MediaController.releaseFuture(controllerFuture)
cancelFlowEvents()
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt
index a761cba5..44bce2b0 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt
@@ -3,15 +3,15 @@ package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioControlsBinding
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
-import ac.mdiq.podcini.playback.PlaybackController
-import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
-import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
-import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
-import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
-import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
+import ac.mdiq.podcini.playback.ServiceStatusHandler
+import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
import ac.mdiq.podcini.storage.model.EpisodeMedia
@@ -21,8 +21,6 @@ import ac.mdiq.podcini.ui.dialog.ShareDialog
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
-import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment
-import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
@@ -41,7 +39,6 @@ import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
-import android.util.Log
import android.view.*
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.widget.EditText
@@ -243,8 +240,8 @@ class VideoplayerActivity : CastEnabledActivity() {
menu.findItem(R.id.remove_from_favorites_item).setVisible(videoEpisodeFragment.isFavorite)
}
- menu.findItem(R.id.set_sleeptimer_item).setVisible(!sleepTimerActive())
- menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerActive())
+ menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
+ menu.findItem(R.id.disable_sleeptimer_item).setVisible(isSleepTimerActive())
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true)
menu.findItem(R.id.audio_controls).setVisible(audioTracks.size >= 2)
@@ -382,7 +379,7 @@ class VideoplayerActivity : CastEnabledActivity() {
}
//Go to x% of video:
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
- seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * duration).toInt())
+ seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * curDurationFB).toInt())
return true
}
return super.onKeyUp(keyCode, event)
@@ -393,11 +390,11 @@ class VideoplayerActivity : CastEnabledActivity() {
private var _binding: AudioControlsBinding? = null
private val binding get() = _binding!!
- private var controller: PlaybackController? = null
+ private var controller: ServiceStatusHandler? = null
@UnstableApi override fun onStart() {
super.onStart()
- controller = object : PlaybackController(requireActivity()) {
+ controller = object : ServiceStatusHandler(requireActivity()) {
override fun loadMediaInfo() {
setupAudioTracks()
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt
index 1b7b4266..173a2e1b 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt
@@ -2,10 +2,10 @@ package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.TimeDialogBinding
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
-import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.service.PlaybackService
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
@@ -51,7 +51,6 @@ import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.*
-import kotlin.time.DurationUnit
class SleepTimerDialog : DialogFragment() {
private var _binding: TimeDialogBinding? = null
@@ -146,7 +145,7 @@ class SleepTimerDialog : DialogFragment() {
val time = if (binding.endEpisode.isChecked) {
val curPosition = curMedia?.getPosition() ?: 0
val duration = curMedia?.getDuration() ?: 0
- val converter = TimeSpeedConverter(curSpeedMultiplier)
+ val converter = TimeSpeedConverter(curSpeedFB)
TimeUnit.MILLISECONDS.toMinutes(converter.convert(max((duration - curPosition).toDouble(), 0.0).toInt()).toLong()) // ms to minutes
} else etxtTime.getText().toString().toLong()
Logd(TAG, "Sleep timer set: $time")
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt
index a18cb09f..37d5df48 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt
@@ -2,12 +2,12 @@ package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
-import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
@@ -76,7 +76,7 @@ import java.util.*
if (!settingCode[2]) binding.global.visibility = View.INVISIBLE
procFlowEvents()
- updateSpeed(FlowEvent.SpeedChangedEvent(curSpeedMultiplier))
+ updateSpeed(FlowEvent.SpeedChangedEvent(curSpeedFB))
}
@UnstableApi override fun onStop() {
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt
index 9e169e4e..db40b000 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt
@@ -3,27 +3,29 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
-import ac.mdiq.podcini.playback.PlaybackController
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
-import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
-import ac.mdiq.podcini.playback.PlaybackController.Companion.fallbackSpeed
-import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
-import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
-import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
-import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
-import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
+import ac.mdiq.podcini.playback.PlaybackServiceStarter
+import ac.mdiq.podcini.playback.ServiceStatusHandler
+import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
+import ac.mdiq.podcini.playback.service.PlaybackService
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.toggleFallbackSpeed
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.receiver.MediaButtonReceiver
-import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.model.Chapter
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable
@@ -47,6 +49,7 @@ import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.app.Activity
+import android.content.ComponentName
import android.content.Intent
import android.os.Bundle
import android.util.Log
@@ -63,12 +66,16 @@ import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
+import androidx.media3.session.MediaController
+import androidx.media3.session.SessionToken
import coil.imageLoader
import coil.request.ErrorResult
import coil.request.ImageRequest
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.elevation.SurfaceColors
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.util.concurrent.MoreExecutors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
@@ -100,7 +107,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
private lateinit var cardViewSeek: CardView
- private var controller: PlaybackController? = null
+ private lateinit var controllerFuture: ListenableFuture
+ private var controller: ServiceStatusHandler? = null
private var seekedToChapterStart = false
// private var currentChapterIndex = -1
@@ -128,7 +136,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
toolbar.setOnMenuItemClickListener(this)
- controller = createController()
+ controller = createHandler()
controller!!.init()
playerUI1 = PlayerUIFragment.newInstance(controller!!)
@@ -152,7 +160,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
return binding.root
}
- fun initDetailedView() {
+ private fun initDetailedView() {
if (playerDetailsFragment == null) {
val fm = requireActivity().supportFragmentManager
val transaction = fm.beginTransaction()
@@ -198,7 +206,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val dividerPos = FloatArray(chapters.size)
for (i in chapters.indices) {
- dividerPos[i] = chapters[i].start / duration.toFloat()
+ dividerPos[i] = chapters[i].start / curDurationFB.toFloat()
}
}
}
@@ -246,9 +254,9 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
}
- private fun createController(): PlaybackController {
- return object : PlaybackController(requireActivity()) {
- override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
+ private fun createHandler(): ServiceStatusHandler {
+ return object : ServiceStatusHandler(requireActivity()) {
+ override fun updatePlayButton(showPlay: Boolean) {
isShowPlay = showPlay
playerUI?.butPlay?.setIsShowPlay(showPlay)
// playerFragment2?.butPlay?.setIsShowPlay(showPlay)
@@ -287,12 +295,21 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
Logd(TAG, "onStart() isCollapsed: $isCollapsed")
super.onStart()
procFlowEvents()
+
+ val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
+ controllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
+ controllerFuture.addListener({
+// mediaController = controllerFuture.get()
+// Logd(TAG, "controllerFuture.addListener: $mediaController")
+ }, MoreExecutors.directExecutor())
+
loadMediaInfo(false)
}
override fun onStop() {
Logd(TAG, "onStop()")
super.onStop()
+ MediaController.releaseFuture(controllerFuture)
cancelFlowEvents()
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
}
@@ -377,8 +394,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
when {
fromUser -> {
val prog: Float = progress / (seekBar.max.toFloat())
- val converter = TimeSpeedConverter(curSpeedMultiplier)
- val position: Int = converter.convert((prog * duration).toInt())
+ val converter = TimeSpeedConverter(curSpeedFB)
+ val position: Int = converter.convert((prog * curDurationFB).toInt())
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(curMedia, position)
if (newChapterIndex > -1) {
// if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) {
@@ -393,7 +410,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${DurationConverter.getDurationStringLong(position)}")
} else binding.txtvSeek.text = DurationConverter.getDurationStringLong(position)
}
- duration != playbackService?.curDuration -> updateUi()
+ curDurationFB != playbackService?.curDuration -> updateUi()
}
}
@@ -414,7 +431,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
seekedToChapterStart = false
} else {
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
- seekTo((prog * duration).toInt())
+ seekTo((prog * curDurationFB).toInt())
}
}
cardViewSeek.scaleX = 1f
@@ -438,8 +455,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO)
if (controller != null) {
- toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!sleepTimerActive())
- toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerActive())
+ toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
+ toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(isSleepTimerActive())
}
(activity as? CastEnabledActivity)?.requestCastButton(toolbar.menu)
}
@@ -457,7 +474,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
playerDetailsFragment?.buildHomeReaderText()
}
R.id.show_video -> {
- controller!!.playPause()
+ playPause()
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
}
R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
@@ -535,10 +552,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val mediaType = media.getMediaType()
if (mediaType == MediaType.AUDIO ||
(mediaType == MediaType.VIDEO && (videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY))) {
- controller!!.ensureService()
+ ensureService()
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
} else {
- controller?.playPause()
+ playPause()
// controller!!.ensureService()
val intent = getPlayerActivityIntent(requireContext(), mediaType)
startActivity(intent)
@@ -557,12 +574,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
super.onViewCreated(view, savedInstanceState)
butPlay?.setOnClickListener {
if (controller == null) return@setOnClickListener
-// val media = curMedia
if (curMedia != null) {
if (curMedia?.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
- controller!!.playPause()
+ playPause()
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
- } else controller!!.playPause()
+ } else playPause()
if (!isControlButtonsSet) {
sbPosition.visibility = View.VISIBLE
isControlButtonsSet = true
@@ -584,7 +600,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
butPlay?.setOnLongClickListener {
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
- if (fallbackSpeed > 0.1f) fallbackSpeed(fallbackSpeed)
+ if (fallbackSpeed > 0.1f) toggleFallbackSpeed(fallbackSpeed)
}
true
}
@@ -609,6 +625,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
true
}
}
+ private fun ensureService() {
+ if (curMedia == null) return
+ if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start()
+ }
private fun speedForward(speed: Float) {
// playbackService?.speedForward(speed)
if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return
@@ -624,7 +644,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
if (controller == null) return@OnClickListener
showTimeLeft = !showTimeLeft
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
- onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPosition, duration))
+ onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPositionFB, curDurationFB))
})
}
fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) {
@@ -634,8 +654,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
@UnstableApi
fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) {
- if (curMedia?.getIdentifier() != event.media?.getIdentifier() || controller == null || curPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) return
- val converter = TimeSpeedConverter(curSpeedMultiplier)
+ if (curMedia?.getIdentifier() != event.media?.getIdentifier() || controller == null || curPositionFB == Playable.INVALID_TIME || curDurationFB == Playable.INVALID_TIME) return
+ val converter = TimeSpeedConverter(curSpeedFB)
val currentPosition: Int = converter.convert(event.position)
val duration: Int = converter.convert(event.duration)
val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt())
@@ -685,14 +705,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
override fun onPause() {
Logd(TAG, "onPause() called")
super.onPause()
- controller?.pause()
+// controller?.pause()
}
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
if (playbackService?.isServiceReady() == true) {
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
- seekTo((prog * duration).toInt())
+ seekTo((prog * curDurationFB).toInt())
}
}
@UnstableApi
@@ -736,8 +756,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
companion object {
- var controller: PlaybackController? = null
- fun newInstance(controller_: PlaybackController) : PlayerUIFragment {
+ var controller: ServiceStatusHandler? = null
+ fun newInstance(controller_: ServiceStatusHandler) : PlayerUIFragment {
controller = controller_
return PlayerUIFragment()
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt
index 26be3d75..be574482 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/ChaptersFragment.kt
@@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
import ac.mdiq.podcini.databinding.SimplechapterItemBinding
-import ac.mdiq.podcini.playback.PlaybackController
+import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.storage.model.EpisodeMedia
@@ -11,8 +11,9 @@ import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.utils.ChapterUtils.getCurrentChapterIndex
import ac.mdiq.podcini.storage.utils.ChapterUtils.loadChapters
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
-import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.storage.model.Chapter
import ac.mdiq.podcini.storage.model.EmbeddedChapterImage
import ac.mdiq.podcini.ui.view.CircularProgressBar
@@ -63,7 +64,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
private lateinit var progressBar: ProgressBar
private lateinit var adapter: ChaptersListAdapter
- private var controller: PlaybackController? = null
+ private var controller: ServiceStatusHandler? = null
private var focusedChapter = -1
private var media: Playable? = null
@@ -97,7 +98,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
adapter = ChaptersListAdapter(requireContext(), object : ChaptersListAdapter.Callback {
override fun onPlayChapterButtonClicked(pos: Int) {
- if (MediaPlayerBase.status != PlayerStatus.PLAYING) controller!!.playPause()
+ if (MediaPlayerBase.status != PlayerStatus.PLAYING) playPause()
val chapter = adapter.getItem(pos)
if (chapter != null) seekTo(chapter.start.toInt())
@@ -111,7 +112,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
val wrapHeight = CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.WRAP_CONTENT)
recyclerView.layoutParams = wrapHeight
- controller = object : PlaybackController(requireActivity()) {
+ controller = object : ServiceStatusHandler(requireActivity()) {
override fun loadMediaInfo() {
this@ChaptersFragment.loadMediaInfo(false)
}
@@ -167,7 +168,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
private fun getCurrentChapter(media: Playable?): Int {
if (controller == null) return -1
- return getCurrentChapterIndex(media, curPosition)
+ return getCurrentChapterIndex(media, curPositionFB)
}
private fun loadMediaInfo(forceRefresh: Boolean) {
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
index 09e5458c..d0b456bb 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt
@@ -7,8 +7,8 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
-import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
import ac.mdiq.podcini.playback.base.InTheatre
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt
index a589c05a..ef23a737 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt
@@ -3,10 +3,10 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
-import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
@@ -93,7 +93,7 @@ class PlayerDetailsFragment : Fragment() {
Logd(TAG, "fragment onCreateView")
shownoteView = binding.webview
- shownoteView.setTimecodeSelectedListener { time: Int? -> seekTo(time!!) }
+ shownoteView.setTimecodeSelectedListener { time: Int -> seekTo(time) }
shownoteView.setPageFinishedListener {
// Restoring the scroll position might not always work
shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
@@ -347,7 +347,7 @@ class PlayerDetailsFragment : Fragment() {
when {
displayedChapterIndex < 1 -> seekTo(0)
- (curPosition - 10000 * curSpeedMultiplier) < curr.start -> {
+ (curPositionFB - 10000 * curSpeedFB) < curr.start -> {
refreshChapterData(displayedChapterIndex - 1)
if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt
index 8b4101b6..137bd438 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QueuesFragment.kt
@@ -6,6 +6,9 @@ import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
import ac.mdiq.podcini.databinding.QueueFragmentBinding
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
+import ac.mdiq.podcini.playback.service.PlaybackService
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.mediaBrowser
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
@@ -38,6 +41,8 @@ import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
+import android.annotation.SuppressLint
+import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
@@ -64,12 +69,16 @@ import androidx.compose.ui.window.Dialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
+import androidx.media3.session.MediaBrowser
+import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
+import com.google.common.util.concurrent.ListenableFuture
+import com.google.common.util.concurrent.MoreExecutors
import com.leinardi.android.speeddial.SpeedDialActionItem
import com.leinardi.android.speeddial.SpeedDialView
import kotlinx.coroutines.*
@@ -104,6 +113,8 @@ import java.util.*
private var showBin: Boolean = false
private var addToQueueActionItem: SpeedDialActionItem? = null
+ private lateinit var browserFuture: ListenableFuture
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
@@ -138,9 +149,11 @@ import java.util.*
queueSpinner.setSelection(queueNames.indexOf(curQueue.name))
queueSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+ val prevQueueSize = curQueue.size()
curQueue = upsertBlk(queues[position]) { it.update() }
toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default")
loadCurQueue(true)
+ playbackService?.notifyCurQueueItemsChanged(Math.max(prevQueueSize, curQueue.size()))
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
@@ -216,6 +229,16 @@ import java.util.*
adapter?.refreshFragPosCallback = ::refreshPosCallback
loadCurQueue(true)
procFlowEvents()
+ val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
+ browserFuture = MediaBrowser.Builder(requireContext(), sessionToken).buildAsync()
+ browserFuture.addListener(
+ {
+ // here we can get the root of media items tree or we can get also the children if it is an album for example.
+ mediaBrowser = browserFuture.get()
+ mediaBrowser?.subscribe("CurQueue", null)
+ },
+ MoreExecutors.directExecutor()
+ )
// if (queueItems.isNotEmpty()) recyclerView.restoreScrollPosition(TAG)
}
@@ -224,6 +247,9 @@ import java.util.*
super.onStop()
adapter?.refreshFragPosCallback = null
cancelFlowEvents()
+ mediaBrowser?.unsubscribe("CurQueue")
+ mediaBrowser = null
+ MediaBrowser.releaseFuture(browserFuture)
val childCount = recyclerView.childCount
for (i in 0 until childCount) {
val child = recyclerView.getChildAt(i)
@@ -327,7 +353,10 @@ import java.util.*
}
}
}
- FlowEvent.QueueEvent.Action.SWITCH_QUEUE -> loadCurQueue(false)
+ FlowEvent.QueueEvent.Action.SWITCH_QUEUE -> {
+ loadCurQueue(false)
+ playbackService?.notifyCurQueueItemsChanged(event.episodes.size)
+ }
FlowEvent.QueueEvent.Action.CLEARED -> {
queueItems.clear()
adapter?.updateItems(queueItems)
@@ -385,6 +414,7 @@ import java.util.*
if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
}
+ @SuppressLint("RestrictedApi")
private fun onKeyUp(event: KeyEvent) {
if (!isAdded || !isVisible || !isMenuVisible) return
@@ -650,10 +680,9 @@ import java.util.*
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
if (queueItems.isNotEmpty()) emptyViewHandler.hide()
queueItems.clear()
- if (showBin) {
- queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
+ if (showBin) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
- } else {
+ else {
curQueue.episodes.clear()
queueItems.addAll(curQueue.episodes)
}
@@ -664,6 +693,7 @@ import java.util.*
adapter?.updateItems(queueItems)
if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
refreshInfoBar()
+// playbackService?.notifyCurQueueItemsChanged()
loadItemsRunning = false
}
}
diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt
index 203c5f68..3e4e1dc7 100644
--- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt
+++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt
@@ -2,16 +2,17 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
-import ac.mdiq.podcini.playback.PlaybackController
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
-import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
+import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
-import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
-import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
-import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
-import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
+import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
@@ -76,7 +77,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
private lateinit var webvDescription: ShownotesWebView
var destroyingDueToReload = false
- var controller: PlaybackController? = null
+ var controller: ServiceStatusHandler? = null
var isFavorite = false
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
@@ -116,9 +117,9 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
return root
}
- @OptIn(UnstableApi::class) private fun newPlaybackController(): PlaybackController {
- return object : PlaybackController(requireActivity()) {
- override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
+ @OptIn(UnstableApi::class) private fun newPlaybackController(): ServiceStatusHandler {
+ return object : ServiceStatusHandler(requireActivity()) {
+ override fun updatePlayButton(showPlay: Boolean) {
Logd(TAG, "updatePlayButtonShowsPlay called")
binding.playButton.setIsShowPlay(showPlay)
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@@ -159,9 +160,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
@UnstableApi
override fun onPause() {
- if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
- if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause()
- }
+// this does nothing
+// if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
+// if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause()
+// }
super.onPause()
}
@@ -286,7 +288,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
binding.durationLabel.setOnClickListener {
showTimeLeft = !showTimeLeft
val media = curMedia ?: return@setOnClickListener
- val converter = TimeSpeedConverter(curSpeedMultiplier)
+ val converter = TimeSpeedConverter(curSpeedFB)
val length: String
if (showTimeLeft) {
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
@@ -423,8 +425,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
@UnstableApi
fun onPlayPause() {
- if (controller == null) return
- controller!!.playPause()
+ playPause()
setupVideoControlsToggler()
}
@@ -482,10 +483,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
private fun onPositionObserverUpdate() {
if (controller == null) return
- val converter = TimeSpeedConverter(curSpeedMultiplier)
- val currentPosition = converter.convert(curPosition)
- val duration_ = converter.convert(duration)
- val remainingTime = converter.convert(duration - curPosition)
+ val converter = TimeSpeedConverter(curSpeedFB)
+ val currentPosition = converter.convert(curPositionFB)
+ val duration_ = converter.convert(curDurationFB)
+ val remainingTime = converter.convert(curDurationFB - curPositionFB)
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
Log.w(TAG, "Could not react to position observer update because of invalid time")
@@ -508,8 +509,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
if (controller == null) return
if (fromUser) {
prog = progress / (seekBar.max.toFloat())
- val converter = TimeSpeedConverter(curSpeedMultiplier)
- val position = converter.convert((prog * duration).toInt())
+ val converter = TimeSpeedConverter(curSpeedFB)
+ val position = converter.convert((prog * curDurationFB).toInt())
binding.seekPositionLabel.text = getDurationStringLong(position)
}
}
@@ -526,7 +527,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
- seekTo((prog * duration).toInt())
+ seekTo((prog * curDurationFB).toInt())
binding.seekCardView.scaleX = 1f
binding.seekCardView.scaleY = 1f
binding.seekCardView.animate()
diff --git a/build.gradle b/build.gradle
index 1f03c6a9..de5ee33a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,7 +4,7 @@ buildscript {
mavenCentral()
gradlePluginPortal()
}
- ext.kotlin_version = '2.0.10'
+ ext.kotlin_version = "$libs.versions.kotlin"
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.android.tools.build:gradle:8.5.2'
diff --git a/changelog.md b/changelog.md
index 58f1636a..8f9a7df6 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,10 @@
+# 6.4.0
+
+* PlaybackService is now a MediaLibraryService and also takes over many functionalities from PlaybackController
+* PlaybackController is renamed to ServiceStatusHandler and is stripped to bare bones
+* enabled Android Auto UI, currently playing episode and episodes in the active queue are shown in the Auto UI
+* added code to handle case where keycode=-1 and keyEvent!=null, attempting to resolve the occassional issue of random start playing
+
# 6.3.7
* inlined some DB writes of Episodes in some routines
diff --git a/fastlane/metadata/android/en-US/changelogs/3020231.txt b/fastlane/metadata/android/en-US/changelogs/3020231.txt
index 0e3f254f..3984807d 100644
--- a/fastlane/metadata/android/en-US/changelogs/3020231.txt
+++ b/fastlane/metadata/android/en-US/changelogs/3020231.txt
@@ -1,6 +1,6 @@
- Version 6.3.7 brings several changes:
+ Version 6.4.0 brings several changes:
-* inlined some DB writes of Episodes in some routines
-* enhanced DB writes in download routine, fixed a write error
-* added a couple more Log.d statements in hope for tracking down the mysterious random playing
-* Kotlin upped to 2.0.10
+* PlaybackService is now a MediaLibraryService and also takes over many functionalities from PlaybackController
+* PlaybackController is renamed to ServiceStatusHandler and is stripped to bare bones
+* enabled Android Auto UI, currently playing episode and episodes in the active queue are shown in the Auto UI
+* added code to handle case where keycode=-1 and keyEvent!=null, attempting to resolve the occassional issue of random start playing
diff --git a/fastlane/metadata/android/en-US/changelogs/3020232.txt b/fastlane/metadata/android/en-US/changelogs/3020232.txt
new file mode 100644
index 00000000..0e3f254f
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/3020232.txt
@@ -0,0 +1,6 @@
+ Version 6.3.7 brings several changes:
+
+* inlined some DB writes of Episodes in some routines
+* enhanced DB writes in download routine, fixed a write error
+* added a couple more Log.d statements in hope for tracking down the mysterious random playing
+* Kotlin upped to 2.0.10
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 169b0a4c..70a44f6d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,6 +1,6 @@
[versions]
-kotlin = "2.0.0"
+kotlin = "2.0.10"
[plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }