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"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020231
versionName "6.3.7"
versionCode 3020232
versionName "6.4.0"
applicationId "ac.mdiq.podcini.R"
def commit = ""
@ -125,6 +125,15 @@ android {
}
}
splits {
abi {
enable true
reset()
include "arm64-v8a" // Specify the ABI you want to split.
universalApk true // This will generate a universal APK that includes all ABIs.
}
}
buildTypes {
release {
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg"
@ -156,7 +165,7 @@ android {
}
dependencies {
def composeBom = platform('androidx.compose:compose-bom:2024.06.00')
def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
implementation composeBom
androidTestImplementation composeBom
@ -215,6 +224,7 @@ dependencies {
// implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
// 5.5.0-b01 is newer than 5.5.0-compose01
implementation 'com.mikepenz:iconics-core:5.5.0-b01'
implementation 'com.mikepenz:iconics-views:5.5.0-b01'
implementation 'com.mikepenz:google-material-typeface:4.0.0.3-kotlin@aar'
@ -243,7 +253,7 @@ dependencies {
androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1"
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.6.1"
androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1"
androidTestImplementation "androidx.test:runner:1.6.1"
androidTestImplementation "androidx.test:runner:1.6.2"
androidTestImplementation "androidx.test:rules:1.6.1"
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'org.awaitility:awaitility:4.2.1'

View File

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

View File

@ -56,7 +56,7 @@
tools:ignore="ExportedService">
<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="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />
</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.curState
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.Prefs.prefPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.FeedPreferences
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.storage.model.Playable
import android.content.Context
import android.media.AudioManager
import android.net.Uri
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock
import android.util.Log
import android.util.Pair
import android.view.SurfaceHolder
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.Volatile
@ -316,6 +317,28 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
}
}
fun buildMetadata(p: Playable): MediaMetadata {
val builder = MediaMetadata.Builder()
.setIsBrowsable(true)
.setIsPlayable(true)
.setArtist(p.getFeedTitle())
.setTitle(p.getEpisodeTitle())
.setAlbumArtist(p.getFeedTitle())
.setDisplayTitle(p.getEpisodeTitle())
.setSubtitle(p.getFeedTitle())
.setArtworkUri(null)
return builder.build()
}
fun buildMediaItem(p: Playable): MediaItem? {
val url = p.getStreamUrl() ?: return null
val metadata = buildMetadata(p)
return MediaItem.Builder()
.setMediaId(url)
.setUri(Uri.parse(url))
.setMediaMetadata(metadata).build()
}
/**
* @param currentPosition current position in a media file in ms
* @param lastPlayedTime timestamp when was media paused

View File

@ -166,17 +166,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
exoPlayer?.setAudioAttributes(b.build(), true)
}
private fun buildMetadata(p: Playable): MediaMetadata {
val builder = MediaMetadata.Builder()
.setArtist(p.getFeedTitle())
.setTitle(p.getEpisodeTitle())
.setAlbumArtist(p.getFeedTitle())
.setDisplayTitle(p.getEpisodeTitle())
.setSubtitle(p.getFeedTitle())
.setArtworkUri(null)
return builder.build()
}
@Throws(IllegalArgumentException::class, IllegalStateException::class)
private fun setDataSource(metadata: MediaMetadata, mediaUrl: String, user: String?, password: String?) {
Logd(TAG, "setDataSource: $mediaUrl")
@ -681,6 +670,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
private var httpDataSourceFactory: OkHttpDataSource.Factory? = null
private var trackSelector: DefaultTrackSelector? = null
var exoPlayer: ExoPlayer? = null
private var exoplayerListener: Listener? = null

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.writeNoMediaPlaying
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.buildMediaItem
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.PlayerStatus
@ -28,6 +30,7 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
@ -74,6 +77,7 @@ import android.webkit.URLUtil
import android.widget.Toast
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player.STATE_ENDED
import androidx.media3.common.Player.STATE_IDLE
@ -93,7 +97,9 @@ import kotlin.math.max
* Controls the MediaPlayer that plays a EpisodeMedia-file
*/
@UnstableApi
class PlaybackService : MediaSessionService() {
class PlaybackService : MediaLibraryService() {
private var mediaSession: MediaLibrarySession? = null
internal var mPlayer: MediaPlayerBase? = null
internal lateinit var taskManager: TaskManager
@ -110,7 +116,6 @@ class PlaybackService : MediaSessionService() {
private var autoSkippedFeedMediaId: String? = null
internal var normalSpeed = 1.0f
private var mediaSession: MediaSession? = null
private val mBinder: IBinder = LocalBinder()
private var clickCount = 0
@ -475,20 +480,162 @@ class PlaybackService : MediaSessionService() {
}
}
inner class LocalBinder : Binder() {
val service: PlaybackService
get() = this@PlaybackService
val rootItem = MediaItem.Builder()
.setMediaId("CurQueue")
.setMediaMetadata(
MediaMetadata.Builder()
.setIsBrowsable(true)
.setIsPlayable(false)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
.setTitle(curQueue.name)
.build())
.build()
val mediaItemsInQueue: MutableList<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 {
Logd(TAG, "Received onUnbind event")
return super.onUnbind(intent)
val mediaLibrarySessionCK = object: MediaLibrarySession.Callback {
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
Logd(TAG, "in MyMediaSessionCallback onConnect")
when {
session.isMediaNotificationController(controller) -> {
Logd(TAG, "MyMediaSessionCallback onConnect isMediaNotificationController")
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
notificationCustomButtons.forEach { commandButton ->
Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
commandButton.sessionCommand?.let(sessionCommands::add)
}
return MediaSession.ConnectionResult.accept(sessionCommands.build(), playerCommands.build())
}
session.isAutoCompanionController(controller) -> {
Logd(TAG, "MyMediaSessionCallback onConnect isAutoCompanionController")
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
notificationCustomButtons.forEach { commandButton ->
Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
commandButton.sessionCommand?.let(sessionCommands::add)
}
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands.build())
.build()
}
else -> {
Logd(TAG, "MyMediaSessionCallback onConnect other controller: ${controller.toString()}")
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
}
}
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
super.onPostConnect(session, controller)
Logd(TAG, "MyMediaSessionCallback onPostConnect")
if (notificationCustomButtons.isNotEmpty()) {
mediaSession?.setCustomLayout(notificationCustomButtons)
// mediaSession?.setCustomLayout(customMediaNotificationProvider.notificationMediaButtons)
}
}
override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture<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() {
super.onCreate()
Logd(TAG, "Service created.")
isRunning = true
playbackService = this
if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED)
@ -523,10 +670,10 @@ class PlaybackService : MediaSessionService() {
recreateMediaPlayer()
if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext)
val intent = packageManager.getLaunchIntentForPackage(packageName)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
mediaSession = MediaSession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else FLAG_UPDATE_CURRENT)
mediaSession = MediaLibrarySession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!, mediaLibrarySessionCK)
.setId(packageName)
.setSessionActivity(pendingIntent)
.setCallback(MyMediaSessionCallback())
.setCustomLayout(notificationCustomButtons)
.build()
}
@ -561,6 +708,7 @@ class PlaybackService : MediaSessionService() {
override fun onDestroy() {
Logd(TAG, "Service is about to be destroyed")
playbackService = null
isRunning = false
currentMediaType = MediaType.UNKNOWN
castStateListener.destroy()
@ -591,99 +739,19 @@ class PlaybackService : MediaSessionService() {
return mediaSession?.player?.playbackState != STATE_IDLE && mediaSession?.player?.playbackState != STATE_ENDED
}
private inner class MyMediaSessionCallback : MediaSession.Callback {
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
Logd(TAG, "in MyMediaSessionCallback onConnect")
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
// .add(NotificationCustomButton.REWIND)
// .add(NotificationCustomButton.FORWARD)
when {
session.isMediaNotificationController(controller) -> {
Logd(TAG, "MyMediaSessionCallback onConnect isMediaNotificationController")
val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
notificationCustomButtons.forEach { commandButton ->
Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
commandButton.sessionCommand?.let(sessionCommands::add)
}
return MediaSession.ConnectionResult.accept(
sessionCommands.build(),
playerCommands.build()
)
}
session.isAutoCompanionController(controller) -> {
Logd(TAG, "MyMediaSessionCallback onConnect isAutoCompanionController")
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands.build())
.build()
}
else -> {
Logd(TAG, "MyMediaSessionCallback onConnect other controller")
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
}
}
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
super.onPostConnect(session, controller)
Logd(TAG, "MyMediaSessionCallback onPostConnect")
if (notificationCustomButtons.isNotEmpty()) {
mediaSession?.setCustomLayout(notificationCustomButtons)
// mediaSession?.setCustomLayout(customMediaNotificationProvider.notificationMediaButtons)
}
}
override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture<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? {
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
return mediaSession
}
override fun onBind(intent: Intent?): IBinder? {
Logd(TAG, "Received onBind event")
return if (intent?.action != null && intent.action == SERVICE_INTERFACE) super.onBind(intent) else mBinder
}
// override fun onBind(intent: Intent?): IBinder? {
// Logd(TAG, "Received onBind event")
// return if (intent?.action != null && intent.action == SERVICE_INTERFACE) super.onBind(intent) else mBinder
// }
// override fun onUnbind(intent: Intent): Boolean {
// Logd(TAG, "Received onUnbind event")
// return super.onUnbind(intent)
// }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
@ -718,6 +786,11 @@ class PlaybackService : MediaSessionService() {
val handled = handleKeycode(keycode, !hardwareButton)
return super.onStartCommand(intent, flags, startId)
}
keyEvent != null && keyEvent.keyCode != -1 -> {
Logd(TAG, "onStartCommand Received button event: ${keyEvent.keyCode}")
val handled = handleKeycode(keyEvent.keyCode, !hardwareButton)
return super.onStartCommand(intent, flags, startId)
}
playable != null -> {
Logd(TAG, "onStartCommand status: $status")
val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false
@ -964,6 +1037,7 @@ class PlaybackService : MediaSessionService() {
private fun onQueueEvent(event: FlowEvent.QueueEvent) {
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}")
notifyCurQueueItemsChanged()
for (e in event.episodes) {
Logd(TAG, "onQueueEvent: ending playback event ${e.title}")
if (e.id == curEpisode?.id) {
@ -974,6 +1048,12 @@ class PlaybackService : MediaSessionService() {
}
}
fun notifyCurQueueItemsChanged(range_: Int = -1) {
val range = if (range_ > 0) range_ else curQueue.size()
Logd(TAG, "notifyCurQueueItemsChanged curQueue: ${curQueue.id}")
mediaSession?.notifyChildrenChanged("CurQueue", range, null)
}
// private fun onVolumeAdaptionChanged(event: FlowEvent.VolumeAdaptionChangedEvent) {
// if (mPlayer != null) updateVolumeIfNecessary(mPlayer!!, event.feedId, event.volumeAdaptionSetting)
// }
@ -1130,6 +1210,11 @@ class PlaybackService : MediaSessionService() {
}
}
inner class LocalBinder : Binder() {
val service: PlaybackService
get() = this@PlaybackService
}
enum class NotificationCustomButton(val customAction: String, val commandButton: CommandButton) {
SKIP(
customAction = CUSTOM_COMMAND_SKIP_ACTION_ID,
@ -1211,6 +1296,9 @@ class PlaybackService : MediaSessionService() {
private const val EXTRA_CODE_VIDEO: Int = 2
private const val EXTRA_CODE_CAST: Int = 3
var playbackService: PlaybackService? = null
var mediaBrowser: MediaBrowser? = null
@JvmField
var isRunning: Boolean = false
@ -1259,6 +1347,88 @@ class PlaybackService : MediaSessionService() {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefFollowQueue.name, value).apply()
}
val curPositionFB: Int
get() = playbackService?.curPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
val curDurationFB: Int
get() = playbackService?.curDuration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
val curSpeedFB: Float
get() = playbackService?.curSpeed ?: getCurrentPlaybackSpeed(curMedia)
val isPlayingVideoLocally: Boolean
get() = when {
isCasting -> false
playbackService != null -> currentMediaType == MediaType.VIDEO
else -> curMedia?.getMediaType() == MediaType.VIDEO
}
var isStartWhenPrepared: Boolean
get() = playbackService?.mPlayer?.startWhenPrepared?.get() ?: false
set(s) {
playbackService?.mPlayer?.startWhenPrepared?.set(s)
}
val mPlayerInfo: MediaPlayerInfo?
get() = playbackService?.mPlayer?.playerInfo
fun seekTo(time: Int) {
playbackService?.mPlayer?.seekTo(time)
}
fun toggleFallbackSpeed(speed: Float) {
if (playbackService != null) {
when (MediaPlayerBase.status) {
PlayerStatus.PLAYING -> {
MediaPlayerBase.status = PlayerStatus.FALLBACK
setToFallbackSpeed(speed)
}
PlayerStatus.FALLBACK -> {
MediaPlayerBase.status = PlayerStatus.PLAYING
setToFallbackSpeed(speed)
}
else -> {}
}
}
}
private fun setToFallbackSpeed(speed: Float) {
if (playbackService?.mPlayer == null || playbackService!!.isSpeedForward) return
if (!playbackService!!.isFallbackSpeed) {
playbackService!!.normalSpeed = playbackService!!.mPlayer!!.getPlaybackSpeed()
playbackService!!.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
} else playbackService!!.mPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
}
fun isSleepTimerActive(): Boolean {
return playbackService?.taskManager?.isSleepTimerActive ?: false
}
fun playPause() {
when (MediaPlayerBase.status) {
PlayerStatus.FALLBACK -> toggleFallbackSpeed(1.0f)
PlayerStatus.PLAYING -> {
playbackService?.mPlayer?.pause(true, reinit = false)
playbackService?.isSpeedForward = false
playbackService?.isFallbackSpeed = false
}
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
playbackService?.mPlayer?.resume()
playbackService?.taskManager?.restartSleepTimer()
}
PlayerStatus.PREPARING -> isStartWhenPrepared = !isStartWhenPrepared
PlayerStatus.INITIALIZED -> {
if (playbackService != null) isStartWhenPrepared = true
playbackService?.mPlayer?.prepare()
playbackService?.taskManager?.restartSleepTimer()
}
else -> Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown")
}
}
fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
val playable = curMedia
if (playable is EpisodeMedia) {

View File

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

View File

@ -1,10 +1,10 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.util.Logd

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.RemoteMedia
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.content.Context

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

View File

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

View File

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

View File

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

View File

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

View File

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

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.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.UsageStatistics
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged

View File

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

View File

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

View File

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

View File

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

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

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]
kotlin = "2.0.0"
kotlin = "2.0.10"
[plugins]
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }