6.4.0 commit

This commit is contained in:
Xilin Jia 2024-08-26 15:12:19 +01:00
parent 770c50efd1
commit ceabe0ece3
26 changed files with 720 additions and 647 deletions

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests" testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020231 versionCode 3020232
versionName "6.3.7" versionName "6.4.0"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" 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 { buildTypes {
release { release {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg"
@ -156,7 +165,7 @@ android {
} }
dependencies { dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.06.00') def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
implementation composeBom implementation composeBom
androidTestImplementation composeBom androidTestImplementation composeBom
@ -215,6 +224,7 @@ dependencies {
// implementation "io.reactivex.rxjava2:rxandroid:2.1.1" // implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.21" 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-core:5.5.0-b01'
implementation 'com.mikepenz:iconics-views: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' 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-core:3.6.1"
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.6.1" androidTestImplementation "androidx.test.espresso:espresso-contrib:3.6.1"
androidTestImplementation "androidx.test.espresso:espresso-intents: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:rules:1.6.1"
androidTestImplementation 'androidx.test.ext:junit:1.2.1' androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'org.awaitility:awaitility:4.2.1' androidTestImplementation 'org.awaitility:awaitility:4.2.1'

View File

@ -1,7 +1,7 @@
package de.test.podcini.playback package de.test.podcini.playback
import ac.mdiq.podcini.R 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.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.base.PlayerStatus
@ -46,7 +46,7 @@ class PlaybackTest {
private var uiTestUtils: UITestUtils? = null private var uiTestUtils: UITestUtils? = null
protected lateinit var context: Context protected lateinit var context: Context
private var controller: PlaybackController? = null private var controller: ServiceStatusHandler? = null
@Before @Before
@Throws(Exception::class) @Throws(Exception::class)
@ -71,7 +71,7 @@ class PlaybackTest {
} }
private fun setupPlaybackController() { private fun setupPlaybackController() {
controller = object : PlaybackController(activityTestRule.activity) { controller = object : ServiceStatusHandler(activityTestRule.activity) {
override fun loadMediaInfo() { override fun loadMediaInfo() {
// Do nothing // Do nothing
} }

View File

@ -56,7 +56,7 @@
tools:ignore="ExportedService"> tools:ignore="ExportedService">
<intent-filter> <intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/> <action android:name="androidx.media3.session.MediaLibraryService"/>
<action android:name="android.media.browse.MediaBrowserService"/> <action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" /> <action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />
</intent-filter> </intent-filter>

View File

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

View File

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

View File

@ -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.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState import ac.mdiq.podcini.playback.base.InTheatre.curState
import ac.mdiq.podcini.preferences.UserPreferences 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.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.FeedPreferences 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.storage.model.MediaType
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.storage.model.Playable
import android.content.Context import android.content.Context
import android.media.AudioManager import android.media.AudioManager
import android.net.Uri
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock import android.net.wifi.WifiManager.WifiLock
import android.util.Log import android.util.Log
import android.util.Pair import android.util.Pair
import android.view.SurfaceHolder import android.view.SurfaceHolder
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.Volatile 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 currentPosition current position in a media file in ms
* @param lastPlayedTime timestamp when was media paused * @param lastPlayedTime timestamp when was media paused

View File

@ -166,17 +166,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
exoPlayer?.setAudioAttributes(b.build(), true) 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) @Throws(IllegalArgumentException::class, IllegalStateException::class)
private fun setDataSource(metadata: MediaMetadata, mediaUrl: String, user: String?, password: String?) { private fun setDataSource(metadata: MediaMetadata, mediaUrl: String, user: String?, password: String?) {
Logd(TAG, "setDataSource: $mediaUrl") Logd(TAG, "setDataSource: $mediaUrl")
@ -681,6 +670,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
private var httpDataSourceFactory: OkHttpDataSource.Factory? = null private var httpDataSourceFactory: OkHttpDataSource.Factory? = null
private var trackSelector: DefaultTrackSelector? = null private var trackSelector: DefaultTrackSelector? = null
var exoPlayer: ExoPlayer? = null var exoPlayer: ExoPlayer? = null
private var exoplayerListener: Listener? = null private var exoplayerListener: Listener? = null

View File

@ -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.loadPlayableFromPreferences
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
import ac.mdiq.podcini.playback.base.MediaPlayerBase 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.MediaPlayerBase.MediaPlayerInfo
import ac.mdiq.podcini.playback.base.MediaPlayerCallback import ac.mdiq.podcini.playback.base.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.PlayerStatus 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
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs 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.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.database.Episodes.addToHistory import ac.mdiq.podcini.storage.database.Episodes.addToHistory
@ -74,6 +77,7 @@ import android.webkit.URLUtil
import android.widget.Toast import android.widget.Toast
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player.STATE_ENDED import androidx.media3.common.Player.STATE_ENDED
import androidx.media3.common.Player.STATE_IDLE import androidx.media3.common.Player.STATE_IDLE
@ -93,7 +97,9 @@ import kotlin.math.max
* Controls the MediaPlayer that plays a EpisodeMedia-file * Controls the MediaPlayer that plays a EpisodeMedia-file
*/ */
@UnstableApi @UnstableApi
class PlaybackService : MediaSessionService() { class PlaybackService : MediaLibraryService() {
private var mediaSession: MediaLibrarySession? = null
internal var mPlayer: MediaPlayerBase? = null internal var mPlayer: MediaPlayerBase? = null
internal lateinit var taskManager: TaskManager internal lateinit var taskManager: TaskManager
@ -110,7 +116,6 @@ class PlaybackService : MediaSessionService() {
private var autoSkippedFeedMediaId: String? = null private var autoSkippedFeedMediaId: String? = null
internal var normalSpeed = 1.0f internal var normalSpeed = 1.0f
private var mediaSession: MediaSession? = null
private val mBinder: IBinder = LocalBinder() private val mBinder: IBinder = LocalBinder()
private var clickCount = 0 private var clickCount = 0
@ -475,20 +480,162 @@ class PlaybackService : MediaSessionService() {
} }
} }
inner class LocalBinder : Binder() { val rootItem = MediaItem.Builder()
val service: PlaybackService .setMediaId("CurQueue")
get() = this@PlaybackService .setMediaMetadata(
MediaMetadata.Builder()
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
.setTitle(curQueue.name)
.build())
.build()
val mediaItemsInQueue: MutableList<MediaItem> by lazy {
val list = mutableListOf<MediaItem>()
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 { val mediaLibrarySessionCK = object: MediaLibrarySession.Callback {
Logd(TAG, "Received onUnbind event") override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
return super.onUnbind(intent) 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<SessionResult> {
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<MediaSession.MediaItemsWithStartPosition> {
Logd(TAG, "MyMediaSessionCallback onPlaybackResumption ")
val settable = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
// 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<LibraryResult<MediaItem>> {
Logd(TAG, "MyMediaSessionCallback onGetItem called mediaId:$mediaId")
return super.onGetItem(session, browser, mediaId)
}
override fun onGetLibraryRoot(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, params: LibraryParams?): ListenableFuture<LibraryResult<MediaItem>> {
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<LibraryResult<ImmutableList<MediaItem>>> {
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<LibraryResult<Void>> {
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onAddMediaItems(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: MutableList<MediaItem>): ListenableFuture<MutableList<MediaItem>> {
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() { override fun onCreate() {
super.onCreate() super.onCreate()
Logd(TAG, "Service created.") Logd(TAG, "Service created.")
isRunning = true isRunning = true
playbackService = this
if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED) registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED)
@ -523,10 +670,10 @@ class PlaybackService : MediaSessionService() {
recreateMediaPlayer() recreateMediaPlayer()
if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext) if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext)
val intent = packageManager.getLaunchIntentForPackage(packageName) val intent = packageManager.getLaunchIntentForPackage(packageName)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) val pendingIntent = PendingIntent.getActivity(this, 0, intent, if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else FLAG_UPDATE_CURRENT)
mediaSession = MediaSession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!) mediaSession = MediaLibrarySession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!, mediaLibrarySessionCK)
.setId(packageName)
.setSessionActivity(pendingIntent) .setSessionActivity(pendingIntent)
.setCallback(MyMediaSessionCallback())
.setCustomLayout(notificationCustomButtons) .setCustomLayout(notificationCustomButtons)
.build() .build()
} }
@ -561,6 +708,7 @@ class PlaybackService : MediaSessionService() {
override fun onDestroy() { override fun onDestroy() {
Logd(TAG, "Service is about to be destroyed") Logd(TAG, "Service is about to be destroyed")
playbackService = null
isRunning = false isRunning = false
currentMediaType = MediaType.UNKNOWN currentMediaType = MediaType.UNKNOWN
castStateListener.destroy() castStateListener.destroy()
@ -591,99 +739,19 @@ class PlaybackService : MediaSessionService() {
return mediaSession?.player?.playbackState != STATE_IDLE && mediaSession?.player?.playbackState != STATE_ENDED return mediaSession?.player?.playbackState != STATE_IDLE && mediaSession?.player?.playbackState != STATE_ENDED
} }
private inner class MyMediaSessionCallback : MediaSession.Callback { override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
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<SessionResult> {
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<MediaSession.MediaItemsWithStartPosition> {
Logd(TAG, "MyMediaSessionCallback onPlaybackResumption ")
val settable = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
// 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? {
return mediaSession return mediaSession
} }
override fun onBind(intent: Intent?): IBinder? { // override fun onBind(intent: Intent?): IBinder? {
Logd(TAG, "Received onBind event") // Logd(TAG, "Received onBind event")
return if (intent?.action != null && intent.action == SERVICE_INTERFACE) super.onBind(intent) else mBinder // 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 { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
@ -718,6 +786,11 @@ class PlaybackService : MediaSessionService() {
val handled = handleKeycode(keycode, !hardwareButton) val handled = handleKeycode(keycode, !hardwareButton)
return super.onStartCommand(intent, flags, startId) 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 -> { playable != null -> {
Logd(TAG, "onStartCommand status: $status") Logd(TAG, "onStartCommand status: $status")
val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false
@ -964,6 +1037,7 @@ class PlaybackService : MediaSessionService() {
private fun onQueueEvent(event: FlowEvent.QueueEvent) { private fun onQueueEvent(event: FlowEvent.QueueEvent) {
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) { if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}") Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}")
notifyCurQueueItemsChanged()
for (e in event.episodes) { for (e in event.episodes) {
Logd(TAG, "onQueueEvent: ending playback event ${e.title}") Logd(TAG, "onQueueEvent: ending playback event ${e.title}")
if (e.id == curEpisode?.id) { 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) { // private fun onVolumeAdaptionChanged(event: FlowEvent.VolumeAdaptionChangedEvent) {
// if (mPlayer != null) updateVolumeIfNecessary(mPlayer!!, event.feedId, event.volumeAdaptionSetting) // 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) { enum class NotificationCustomButton(val customAction: String, val commandButton: CommandButton) {
SKIP( SKIP(
customAction = CUSTOM_COMMAND_SKIP_ACTION_ID, 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_VIDEO: Int = 2
private const val EXTRA_CODE_CAST: Int = 3 private const val EXTRA_CODE_CAST: Int = 3
var playbackService: PlaybackService? = null
var mediaBrowser: MediaBrowser? = null
@JvmField @JvmField
var isRunning: Boolean = false var isRunning: Boolean = false
@ -1259,6 +1347,88 @@ class PlaybackService : MediaSessionService() {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefFollowQueue.name, value).apply() 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) { fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
val playable = curMedia val playable = curMedia
if (playable is EpisodeMedia) { if (playable is EpisodeMedia) {

View File

@ -1,17 +1,15 @@
package ac.mdiq.podcini.ui.actions.actionbutton package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre 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.realm
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.MediaType 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.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent

View File

@ -1,10 +1,10 @@
package ac.mdiq.podcini.ui.actions.actionbutton package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre 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.Episode
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd

View File

@ -10,7 +10,7 @@ import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.RemoteMedia import ac.mdiq.podcini.storage.model.RemoteMedia
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed 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.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
import android.content.Context import android.content.Context

View File

@ -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.feed.discovery.ItunesTopListLoader
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.playback.cast.CastEnabledActivity 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.ThemeSwitcher.getNoTitleTheme
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer 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 ac.mdiq.podcini.util.event.FlowEvent
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
@ -65,8 +63,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager 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.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar 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.*
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
@ -96,7 +90,7 @@ class MainActivity : CastEnabledActivity() {
private lateinit var navDrawerFragment: NavDrawerFragment private lateinit var navDrawerFragment: NavDrawerFragment
private lateinit var audioPlayerFragment: AudioPlayerFragment private lateinit var audioPlayerFragment: AudioPlayerFragment
private lateinit var audioPlayerView: View private lateinit var audioPlayerView: View
private lateinit var controllerFuture: ListenableFuture<MediaController> // private lateinit var controllerFuture: ListenableFuture<MediaController>
private lateinit var navDrawer: View private lateinit var navDrawer: View
private lateinit var dummyView : View private lateinit var dummyView : View
lateinit var bottomSheet: LockableBottomSheetBehavior<*> lateinit var bottomSheet: LockableBottomSheetBehavior<*>
@ -529,19 +523,20 @@ class MainActivity : CastEnabledActivity() {
procFlowEvents() procFlowEvents()
RatingDialog.init(this) RatingDialog.init(this)
val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java)) // val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() // controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
controllerFuture.addListener({ // controllerFuture.addListener({
// Call controllerFuture.get() to retrieve the MediaController. // // Call controllerFuture.get() to retrieve the MediaController.
// MediaController implements the Player interface, so it can be // // MediaController implements the Player interface, so it can be
// attached to the PlayerView UI component. // // attached to the PlayerView UI component.
// playerView.setPlayer(controllerFuture.get()) //// playerView.setPlayer(controllerFuture.get())
}, MoreExecutors.directExecutor()) // val player = controllerFuture.get()
// }, MoreExecutors.directExecutor())
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
MediaController.releaseFuture(controllerFuture) // MediaController.releaseFuture(controllerFuture)
cancelFlowEvents() cancelFlowEvents()
} }

View File

@ -3,15 +3,15 @@ package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioControlsBinding import ac.mdiq.podcini.databinding.AudioControlsBinding
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
import ac.mdiq.podcini.playback.PlaybackController import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
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.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.cast.CastEnabledActivity 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.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.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.storage.database.Episodes.setFavorite import ac.mdiq.podcini.storage.database.Episodes.setFavorite
import ac.mdiq.podcini.storage.model.EpisodeMedia 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.SleepTimerDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.fragment.ChaptersFragment 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.fragment.VideoEpisodeFragment
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.util.IntentUtils.openInBrowser import ac.mdiq.podcini.util.IntentUtils.openInBrowser
@ -41,7 +39,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import android.view.* import android.view.*
import android.view.MenuItem.SHOW_AS_ACTION_NEVER import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.widget.EditText 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.remove_from_favorites_item).setVisible(videoEpisodeFragment.isFavorite)
} }
menu.findItem(R.id.set_sleeptimer_item).setVisible(!sleepTimerActive()) menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerActive()) menu.findItem(R.id.disable_sleeptimer_item).setVisible(isSleepTimerActive())
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true) menu.findItem(R.id.player_switch_to_audio_only).setVisible(true)
menu.findItem(R.id.audio_controls).setVisible(audioTracks.size >= 2) menu.findItem(R.id.audio_controls).setVisible(audioTracks.size >= 2)
@ -382,7 +379,7 @@ class VideoplayerActivity : CastEnabledActivity() {
} }
//Go to x% of video: //Go to x% of video:
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) { 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 true
} }
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
@ -393,11 +390,11 @@ class VideoplayerActivity : CastEnabledActivity() {
private var _binding: AudioControlsBinding? = null private var _binding: AudioControlsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private var controller: PlaybackController? = null private var controller: ServiceStatusHandler? = null
@UnstableApi override fun onStart() { @UnstableApi override fun onStart() {
super.onStart() super.onStart()
controller = object : PlaybackController(requireActivity()) { controller = object : ServiceStatusHandler(requireActivity()) {
override fun loadMediaInfo() { override fun loadMediaInfo() {
setupAudioTracks() setupAudioTracks()
} }

View File

@ -2,10 +2,10 @@ package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.TimeDialogBinding 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.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.service.PlaybackService 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.autoEnable
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
@ -51,7 +51,6 @@ import kotlinx.coroutines.launch
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.* import kotlin.math.*
import kotlin.time.DurationUnit
class SleepTimerDialog : DialogFragment() { class SleepTimerDialog : DialogFragment() {
private var _binding: TimeDialogBinding? = null private var _binding: TimeDialogBinding? = null
@ -146,7 +145,7 @@ class SleepTimerDialog : DialogFragment() {
val time = if (binding.endEpisode.isChecked) { val time = if (binding.endEpisode.isChecked) {
val curPosition = curMedia?.getPosition() ?: 0 val curPosition = curMedia?.getPosition() ?: 0
val duration = curMedia?.getDuration() ?: 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 TimeUnit.MILLISECONDS.toMinutes(converter.convert(max((duration - curPosition).toDouble(), 0.0).toInt()).toLong()) // ms to minutes
} else etxtTime.getText().toString().toLong() } else etxtTime.getText().toString().toLong()
Logd(TAG, "Sleep timer set: $time") Logd(TAG, "Sleep timer set: $time")

View File

@ -2,12 +2,12 @@ package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding 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.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curState 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.currentMediaType
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
@ -76,7 +76,7 @@ import java.util.*
if (!settingCode[2]) binding.global.visibility = View.INVISIBLE if (!settingCode[2]) binding.global.visibility = View.INVISIBLE
procFlowEvents() procFlowEvents()
updateSpeed(FlowEvent.SpeedChangedEvent(curSpeedMultiplier)) updateSpeed(FlowEvent.SpeedChangedEvent(curSpeedFB))
} }
@UnstableApi override fun onStop() { @UnstableApi override fun onStop() {

View File

@ -3,27 +3,29 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
import ac.mdiq.podcini.playback.PlaybackController import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
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.base.InTheatre.curEpisode import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity 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
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.receiver.MediaButtonReceiver 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.Chapter
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable 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.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
import android.app.Activity import android.app.Activity
import android.content.ComponentName
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -63,12 +66,16 @@ import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import coil.imageLoader import coil.imageLoader
import coil.request.ErrorResult import coil.request.ErrorResult
import coil.request.ImageRequest import coil.request.ImageRequest
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.elevation.SurfaceColors 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.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -100,7 +107,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
private lateinit var cardViewSeek: CardView private lateinit var cardViewSeek: CardView
private var controller: PlaybackController? = null private lateinit var controllerFuture: ListenableFuture<MediaController>
private var controller: ServiceStatusHandler? = null
private var seekedToChapterStart = false private var seekedToChapterStart = false
// private var currentChapterIndex = -1 // private var currentChapterIndex = -1
@ -128,7 +136,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
} }
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
controller = createController() controller = createHandler()
controller!!.init() controller!!.init()
playerUI1 = PlayerUIFragment.newInstance(controller!!) playerUI1 = PlayerUIFragment.newInstance(controller!!)
@ -152,7 +160,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
return binding.root return binding.root
} }
fun initDetailedView() { private fun initDetailedView() {
if (playerDetailsFragment == null) { if (playerDetailsFragment == null) {
val fm = requireActivity().supportFragmentManager val fm = requireActivity().supportFragmentManager
val transaction = fm.beginTransaction() val transaction = fm.beginTransaction()
@ -198,7 +206,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val dividerPos = FloatArray(chapters.size) val dividerPos = FloatArray(chapters.size)
for (i in chapters.indices) { 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 { private fun createHandler(): ServiceStatusHandler {
return object : PlaybackController(requireActivity()) { return object : ServiceStatusHandler(requireActivity()) {
override fun updatePlayButtonShowsPlay(showPlay: Boolean) { override fun updatePlayButton(showPlay: Boolean) {
isShowPlay = showPlay isShowPlay = showPlay
playerUI?.butPlay?.setIsShowPlay(showPlay) playerUI?.butPlay?.setIsShowPlay(showPlay)
// playerFragment2?.butPlay?.setIsShowPlay(showPlay) // playerFragment2?.butPlay?.setIsShowPlay(showPlay)
@ -287,12 +295,21 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
Logd(TAG, "onStart() isCollapsed: $isCollapsed") Logd(TAG, "onStart() isCollapsed: $isCollapsed")
super.onStart() super.onStart()
procFlowEvents() 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) loadMediaInfo(false)
} }
override fun onStop() { override fun onStop() {
Logd(TAG, "onStop()") Logd(TAG, "onStop()")
super.onStop() super.onStop()
MediaController.releaseFuture(controllerFuture)
cancelFlowEvents() cancelFlowEvents()
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates // progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
} }
@ -377,8 +394,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
when { when {
fromUser -> { fromUser -> {
val prog: Float = progress / (seekBar.max.toFloat()) val prog: Float = progress / (seekBar.max.toFloat())
val converter = TimeSpeedConverter(curSpeedMultiplier) val converter = TimeSpeedConverter(curSpeedFB)
val position: Int = converter.convert((prog * duration).toInt()) val position: Int = converter.convert((prog * curDurationFB).toInt())
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(curMedia, position) val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(curMedia, position)
if (newChapterIndex > -1) { if (newChapterIndex > -1) {
// if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) { // 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)}") binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${DurationConverter.getDurationStringLong(position)}")
} else binding.txtvSeek.text = 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 seekedToChapterStart = false
} else { } else {
val prog: Float = seekBar.progress / (seekBar.max.toFloat()) val prog: Float = seekBar.progress / (seekBar.max.toFloat())
seekTo((prog * duration).toInt()) seekTo((prog * curDurationFB).toInt())
} }
} }
cardViewSeek.scaleX = 1f cardViewSeek.scaleX = 1f
@ -438,8 +455,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO) toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO)
if (controller != null) { if (controller != null) {
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!sleepTimerActive()) toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerActive()) toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(isSleepTimerActive())
} }
(activity as? CastEnabledActivity)?.requestCastButton(toolbar.menu) (activity as? CastEnabledActivity)?.requestCastButton(toolbar.menu)
} }
@ -457,7 +474,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
playerDetailsFragment?.buildHomeReaderText() playerDetailsFragment?.buildHomeReaderText()
} }
R.id.show_video -> { R.id.show_video -> {
controller!!.playPause() playPause()
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start() VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
} }
R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog") 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() val mediaType = media.getMediaType()
if (mediaType == MediaType.AUDIO || if (mediaType == MediaType.AUDIO ||
(mediaType == MediaType.VIDEO && (videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY))) { (mediaType == MediaType.VIDEO && (videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY))) {
controller!!.ensureService() ensureService()
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
} else { } else {
controller?.playPause() playPause()
// controller!!.ensureService() // controller!!.ensureService()
val intent = getPlayerActivityIntent(requireContext(), mediaType) val intent = getPlayerActivityIntent(requireContext(), mediaType)
startActivity(intent) startActivity(intent)
@ -557,12 +574,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
butPlay?.setOnClickListener { butPlay?.setOnClickListener {
if (controller == null) return@setOnClickListener if (controller == null) return@setOnClickListener
// val media = curMedia
if (curMedia != null) { if (curMedia != null) {
if (curMedia?.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) { if (curMedia?.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
controller!!.playPause() playPause()
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType())) requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
} else controller!!.playPause() } else playPause()
if (!isControlButtonsSet) { if (!isControlButtonsSet) {
sbPosition.visibility = View.VISIBLE sbPosition.visibility = View.VISIBLE
isControlButtonsSet = true isControlButtonsSet = true
@ -584,7 +600,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
butPlay?.setOnLongClickListener { butPlay?.setOnLongClickListener {
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) { if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) fallbackSpeed(fallbackSpeed) if (fallbackSpeed > 0.1f) toggleFallbackSpeed(fallbackSpeed)
} }
true true
} }
@ -609,6 +625,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
true true
} }
} }
private fun ensureService() {
if (curMedia == null) return
if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start()
}
private fun speedForward(speed: Float) { private fun speedForward(speed: Float) {
// playbackService?.speedForward(speed) // playbackService?.speedForward(speed)
if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return
@ -624,7 +644,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
if (controller == null) return@OnClickListener if (controller == null) return@OnClickListener
showTimeLeft = !showTimeLeft showTimeLeft = !showTimeLeft
UserPreferences.setShowRemainTimeSetting(showTimeLeft) UserPreferences.setShowRemainTimeSetting(showTimeLeft)
onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPosition, duration)) onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPositionFB, curDurationFB))
}) })
} }
fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) { fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) {
@ -634,8 +654,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
} }
@UnstableApi @UnstableApi
fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) { fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) {
if (curMedia?.getIdentifier() != event.media?.getIdentifier() || controller == null || curPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) return if (curMedia?.getIdentifier() != event.media?.getIdentifier() || controller == null || curPositionFB == Playable.INVALID_TIME || curDurationFB == Playable.INVALID_TIME) return
val converter = TimeSpeedConverter(curSpeedMultiplier) val converter = TimeSpeedConverter(curSpeedFB)
val currentPosition: Int = converter.convert(event.position) val currentPosition: Int = converter.convert(event.position)
val duration: Int = converter.convert(event.duration) val duration: Int = converter.convert(event.duration)
val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt()) 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() { override fun onPause() {
Logd(TAG, "onPause() called") Logd(TAG, "onPause() called")
super.onPause() super.onPause()
controller?.pause() // controller?.pause()
} }
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} @OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
override fun onStartTrackingTouch(seekBar: SeekBar) {} override fun onStartTrackingTouch(seekBar: SeekBar) {}
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) { @OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
if (playbackService?.isServiceReady() == true) { if (playbackService?.isServiceReady() == true) {
val prog: Float = seekBar.progress / (seekBar.max.toFloat()) val prog: Float = seekBar.progress / (seekBar.max.toFloat())
seekTo((prog * duration).toInt()) seekTo((prog * curDurationFB).toInt())
} }
} }
@UnstableApi @UnstableApi
@ -736,8 +756,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
} }
companion object { companion object {
var controller: PlaybackController? = null var controller: ServiceStatusHandler? = null
fun newInstance(controller_: PlaybackController) : PlayerUIFragment { fun newInstance(controller_: ServiceStatusHandler) : PlayerUIFragment {
controller = controller_ controller = controller_
return PlayerUIFragment() return PlayerUIFragment()
} }

View File

@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
import ac.mdiq.podcini.databinding.SimplechapterItemBinding 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.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.storage.model.EpisodeMedia 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.getCurrentChapterIndex
import ac.mdiq.podcini.storage.utils.ChapterUtils.loadChapters import ac.mdiq.podcini.storage.utils.ChapterUtils.loadChapters
import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo 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.Chapter
import ac.mdiq.podcini.storage.model.EmbeddedChapterImage import ac.mdiq.podcini.storage.model.EmbeddedChapterImage
import ac.mdiq.podcini.ui.view.CircularProgressBar import ac.mdiq.podcini.ui.view.CircularProgressBar
@ -63,7 +64,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
private lateinit var progressBar: ProgressBar private lateinit var progressBar: ProgressBar
private lateinit var adapter: ChaptersListAdapter private lateinit var adapter: ChaptersListAdapter
private var controller: PlaybackController? = null private var controller: ServiceStatusHandler? = null
private var focusedChapter = -1 private var focusedChapter = -1
private var media: Playable? = null private var media: Playable? = null
@ -97,7 +98,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
adapter = ChaptersListAdapter(requireContext(), object : ChaptersListAdapter.Callback { adapter = ChaptersListAdapter(requireContext(), object : ChaptersListAdapter.Callback {
override fun onPlayChapterButtonClicked(pos: Int) { override fun onPlayChapterButtonClicked(pos: Int) {
if (MediaPlayerBase.status != PlayerStatus.PLAYING) controller!!.playPause() if (MediaPlayerBase.status != PlayerStatus.PLAYING) playPause()
val chapter = adapter.getItem(pos) val chapter = adapter.getItem(pos)
if (chapter != null) seekTo(chapter.start.toInt()) 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) val wrapHeight = CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.WRAP_CONTENT)
recyclerView.layoutParams = wrapHeight recyclerView.layoutParams = wrapHeight
controller = object : PlaybackController(requireActivity()) { controller = object : ServiceStatusHandler(requireActivity()) {
override fun loadMediaInfo() { override fun loadMediaInfo() {
this@ChaptersFragment.loadMediaInfo(false) this@ChaptersFragment.loadMediaInfo(false)
} }
@ -167,7 +168,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
private fun getCurrentChapter(media: Playable?): Int { private fun getCurrentChapter(media: Playable?): Int {
if (controller == null) return -1 if (controller == null) return -1
return getCurrentChapterIndex(media, curPosition) return getCurrentChapterIndex(media, curPositionFB)
} }
private fun loadMediaInfo(forceRefresh: Boolean) { private fun loadMediaInfo(forceRefresh: Boolean) {

View File

@ -7,8 +7,8 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.storage.utils.ImageResourceUtils import ac.mdiq.podcini.storage.utils.ImageResourceUtils
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
import ac.mdiq.podcini.playback.base.InTheatre.curMedia 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.base.InTheatre
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.UsageStatistics import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged import ac.mdiq.podcini.storage.database.RealmDB.unmanaged

View File

@ -3,10 +3,10 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource 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.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.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.*
@ -93,7 +93,7 @@ class PlayerDetailsFragment : Fragment() {
Logd(TAG, "fragment onCreateView") Logd(TAG, "fragment onCreateView")
shownoteView = binding.webview shownoteView = binding.webview
shownoteView.setTimecodeSelectedListener { time: Int? -> seekTo(time!!) } shownoteView.setTimecodeSelectedListener { time: Int -> seekTo(time) }
shownoteView.setPageFinishedListener { shownoteView.setPageFinishedListener {
// Restoring the scroll position might not always work // Restoring the scroll position might not always work
shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50) shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
@ -347,7 +347,7 @@ class PlayerDetailsFragment : Fragment() {
when { when {
displayedChapterIndex < 1 -> seekTo(0) displayedChapterIndex < 1 -> seekTo(0)
(curPosition - 10000 * curSpeedMultiplier) < curr.start -> { (curPositionFB - 10000 * curSpeedFB) < curr.start -> {
refreshChapterData(displayedChapterIndex - 1) refreshChapterData(displayedChapterIndex - 1)
if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt()) if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
} }

View File

@ -6,6 +6,9 @@ import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
import ac.mdiq.podcini.databinding.QueueFragmentBinding import ac.mdiq.podcini.databinding.QueueFragmentBinding
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed 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.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Queues.clearQueue import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted 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.Logd
import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent import ac.mdiq.podcini.util.event.FlowEvent
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences import android.content.SharedPreferences
@ -64,12 +69,16 @@ import androidx.compose.ui.window.Dialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi 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.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar 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.SpeedDialActionItem
import com.leinardi.android.speeddial.SpeedDialView import com.leinardi.android.speeddial.SpeedDialView
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -104,6 +113,8 @@ import java.util.*
private var showBin: Boolean = false private var showBin: Boolean = false
private var addToQueueActionItem: SpeedDialActionItem? = null private var addToQueueActionItem: SpeedDialActionItem? = null
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
retainInstance = true retainInstance = true
@ -138,9 +149,11 @@ import java.util.*
queueSpinner.setSelection(queueNames.indexOf(curQueue.name)) queueSpinner.setSelection(queueNames.indexOf(curQueue.name))
queueSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { queueSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val prevQueueSize = curQueue.size()
curQueue = upsertBlk(queues[position]) { it.update() } curQueue = upsertBlk(queues[position]) { it.update() }
toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default")
loadCurQueue(true) loadCurQueue(true)
playbackService?.notifyCurQueueItemsChanged(Math.max(prevQueueSize, curQueue.size()))
} }
override fun onNothingSelected(parent: AdapterView<*>?) {} override fun onNothingSelected(parent: AdapterView<*>?) {}
} }
@ -216,6 +229,16 @@ import java.util.*
adapter?.refreshFragPosCallback = ::refreshPosCallback adapter?.refreshFragPosCallback = ::refreshPosCallback
loadCurQueue(true) loadCurQueue(true)
procFlowEvents() 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) // if (queueItems.isNotEmpty()) recyclerView.restoreScrollPosition(TAG)
} }
@ -224,6 +247,9 @@ import java.util.*
super.onStop() super.onStop()
adapter?.refreshFragPosCallback = null adapter?.refreshFragPosCallback = null
cancelFlowEvents() cancelFlowEvents()
mediaBrowser?.unsubscribe("CurQueue")
mediaBrowser = null
MediaBrowser.releaseFuture(browserFuture)
val childCount = recyclerView.childCount val childCount = recyclerView.childCount
for (i in 0 until childCount) { for (i in 0 until childCount) {
val child = recyclerView.getChildAt(i) 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 -> { FlowEvent.QueueEvent.Action.CLEARED -> {
queueItems.clear() queueItems.clear()
adapter?.updateItems(queueItems) adapter?.updateItems(queueItems)
@ -385,6 +414,7 @@ import java.util.*
if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon()) if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
} }
@SuppressLint("RestrictedApi")
private fun onKeyUp(event: KeyEvent) { private fun onKeyUp(event: KeyEvent) {
if (!isAdded || !isVisible || !isMenuVisible) return if (!isAdded || !isVisible || !isMenuVisible) return
@ -650,10 +680,9 @@ import java.util.*
while (curQueue.name.isEmpty()) runBlocking { delay(100) } while (curQueue.name.isEmpty()) runBlocking { delay(100) }
if (queueItems.isNotEmpty()) emptyViewHandler.hide() if (queueItems.isNotEmpty()) emptyViewHandler.hide()
queueItems.clear() queueItems.clear()
if (showBin) { if (showBin) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) }) .find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
} else { else {
curQueue.episodes.clear() curQueue.episodes.clear()
queueItems.addAll(curQueue.episodes) queueItems.addAll(curQueue.episodes)
} }
@ -664,6 +693,7 @@ import java.util.*
adapter?.updateItems(queueItems) adapter?.updateItems(queueItems)
if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG) if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
refreshInfoBar() refreshInfoBar()
// playbackService?.notifyCurQueueItemsChanged()
loadItemsRunning = false loadItemsRunning = false
} }
} }

View File

@ -2,16 +2,17 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
import ac.mdiq.podcini.playback.PlaybackController import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
import ac.mdiq.podcini.playback.base.InTheatre.curMedia 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.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus 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.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
@ -76,7 +77,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
private lateinit var webvDescription: ShownotesWebView private lateinit var webvDescription: ShownotesWebView
var destroyingDueToReload = false var destroyingDueToReload = false
var controller: PlaybackController? = null var controller: ServiceStatusHandler? = null
var isFavorite = false var isFavorite = false
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent -> private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
@ -116,9 +117,9 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
return root return root
} }
@OptIn(UnstableApi::class) private fun newPlaybackController(): PlaybackController { @OptIn(UnstableApi::class) private fun newPlaybackController(): ServiceStatusHandler {
return object : PlaybackController(requireActivity()) { return object : ServiceStatusHandler(requireActivity()) {
override fun updatePlayButtonShowsPlay(showPlay: Boolean) { override fun updatePlayButton(showPlay: Boolean) {
Logd(TAG, "updatePlayButtonShowsPlay called") Logd(TAG, "updatePlayButtonShowsPlay called")
binding.playButton.setIsShowPlay(showPlay) binding.playButton.setIsShowPlay(showPlay)
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@ -159,9 +160,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
@UnstableApi @UnstableApi
override fun onPause() { override fun onPause() {
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) { // this does nothing
if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause() // if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
} // if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause()
// }
super.onPause() super.onPause()
} }
@ -286,7 +288,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
binding.durationLabel.setOnClickListener { binding.durationLabel.setOnClickListener {
showTimeLeft = !showTimeLeft showTimeLeft = !showTimeLeft
val media = curMedia ?: return@setOnClickListener val media = curMedia ?: return@setOnClickListener
val converter = TimeSpeedConverter(curSpeedMultiplier) val converter = TimeSpeedConverter(curSpeedFB)
val length: String val length: String
if (showTimeLeft) { if (showTimeLeft) {
val remainingTime = converter.convert(media.getDuration() - media.getPosition()) val remainingTime = converter.convert(media.getDuration() - media.getPosition())
@ -423,8 +425,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
@UnstableApi @UnstableApi
fun onPlayPause() { fun onPlayPause() {
if (controller == null) return playPause()
controller!!.playPause()
setupVideoControlsToggler() setupVideoControlsToggler()
} }
@ -482,10 +483,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
private fun onPositionObserverUpdate() { private fun onPositionObserverUpdate() {
if (controller == null) return if (controller == null) return
val converter = TimeSpeedConverter(curSpeedMultiplier) val converter = TimeSpeedConverter(curSpeedFB)
val currentPosition = converter.convert(curPosition) val currentPosition = converter.convert(curPositionFB)
val duration_ = converter.convert(duration) val duration_ = converter.convert(curDurationFB)
val remainingTime = converter.convert(duration - curPosition) val remainingTime = converter.convert(curDurationFB - curPositionFB)
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); // Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) { if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
Log.w(TAG, "Could not react to position observer update because of 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 (controller == null) return
if (fromUser) { if (fromUser) {
prog = progress / (seekBar.max.toFloat()) prog = progress / (seekBar.max.toFloat())
val converter = TimeSpeedConverter(curSpeedMultiplier) val converter = TimeSpeedConverter(curSpeedFB)
val position = converter.convert((prog * duration).toInt()) val position = converter.convert((prog * curDurationFB).toInt())
binding.seekPositionLabel.text = getDurationStringLong(position) binding.seekPositionLabel.text = getDurationStringLong(position)
} }
} }
@ -526,7 +527,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
} }
override fun onStopTrackingTouch(seekBar: SeekBar) { override fun onStopTrackingTouch(seekBar: SeekBar) {
seekTo((prog * duration).toInt()) seekTo((prog * curDurationFB).toInt())
binding.seekCardView.scaleX = 1f binding.seekCardView.scaleX = 1f
binding.seekCardView.scaleY = 1f binding.seekCardView.scaleY = 1f
binding.seekCardView.animate() binding.seekCardView.animate()

View File

@ -4,7 +4,7 @@ buildscript {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
} }
ext.kotlin_version = '2.0.10' ext.kotlin_version = "$libs.versions.kotlin"
dependencies { dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.android.tools.build:gradle:8.5.2' classpath 'com.android.tools.build:gradle:8.5.2'

View File

@ -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 # 6.3.7
* inlined some DB writes of Episodes in some routines * inlined some DB writes of Episodes in some routines

View File

@ -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 * PlaybackService is now a MediaLibraryService and also takes over many functionalities from PlaybackController
* enhanced DB writes in download routine, fixed a write error * PlaybackController is renamed to ServiceStatusHandler and is stripped to bare bones
* added a couple more Log.d statements in hope for tracking down the mysterious random playing * enabled Android Auto UI, currently playing episode and episodes in the active queue are shown in the Auto UI
* Kotlin upped to 2.0.10 * added code to handle case where keycode=-1 and keyEvent!=null, attempting to resolve the occassional issue of random start playing

View File

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

View File

@ -1,6 +1,6 @@
[versions] [versions]
kotlin = "2.0.0" kotlin = "2.0.10"
[plugins] [plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }