fix(widget): resume progress updates during playback

The widget was only updating on play/pause state changes, so the timer
did not advance while playback continued. Added a Handler loop in
MediaService that updates the widget every second while playback is
running and clears it when playback stops, ensuring the progress bar
refreshes regularly.

Co-Authored-By: Firehawk <firehawk@opayq.net>
This commit is contained in:
mucahit-kaya 2025-09-18 12:26:43 +02:00
parent e81e1a5356
commit b79cfa4af0
3 changed files with 111 additions and 0 deletions

View file

@ -8,6 +8,8 @@ import android.content.Intent
import android.os.Binder import android.os.Binder
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.os.Handler
import android.os.Looper
import androidx.media3.common.* import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
@ -40,6 +42,18 @@ class MediaService : MediaLibraryService() {
lateinit var equalizerManager: EqualizerManager lateinit var equalizerManager: EqualizerManager
private var customLayout = ImmutableList.of<CommandButton>() private var customLayout = ImmutableList.of<CommandButton>()
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
private var widgetUpdateScheduled = false
private val widgetUpdateRunnable = object : Runnable {
override fun run() {
if (!player.isPlaying) {
widgetUpdateScheduled = false
return
}
updateWidget()
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
}
}
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
fun getEqualizerManager(): EqualizerManager { fun getEqualizerManager(): EqualizerManager {
@ -81,6 +95,7 @@ class MediaService : MediaLibraryService() {
override fun onDestroy() { override fun onDestroy() {
equalizerManager.release() equalizerManager.release()
stopWidgetUpdates()
releasePlayer() releasePlayer()
super.onDestroy() super.onDestroy()
} }
@ -281,6 +296,11 @@ class MediaService : MediaLibraryService() {
} else { } else {
MediaManager.scrobble(player.currentMediaItem, false) MediaManager.scrobble(player.currentMediaItem, false)
} }
if (isPlaying) {
scheduleWidgetUpdates()
} else {
stopWidgetUpdates()
}
updateWidget() updateWidget()
} }
@ -327,6 +347,9 @@ class MediaService : MediaLibraryService() {
mediaLibrarySession.setCustomLayout(customLayout) mediaLibrarySession.setCustomLayout(customLayout)
} }
}) })
if (player.isPlaying) {
scheduleWidgetUpdates()
}
} }
private fun setPlayer(player: Player) { private fun setPlayer(player: Player) {
@ -407,9 +430,23 @@ class MediaService : MediaLibraryService() {
) )
} }
private fun scheduleWidgetUpdates() {
if (widgetUpdateScheduled) return
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
widgetUpdateScheduled = true
}
private fun stopWidgetUpdates() {
if (!widgetUpdateScheduled) return
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
widgetUpdateScheduled = false
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
private fun getMediaSourceFactory() = private fun getMediaSourceFactory() =
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
} }
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L

View file

@ -6,6 +6,8 @@ import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
import android.os.IBinder import android.os.IBinder
import android.os.Handler
import android.os.Looper
import androidx.media3.cast.CastPlayer import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
@ -50,6 +52,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
companion object { companion object {
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER" const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
} }
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
private var widgetUpdateScheduled = false
private val widgetUpdateRunnable = object : Runnable {
override fun run() {
if (!player.isPlaying) {
widgetUpdateScheduled = false
return
}
updateWidget()
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -81,6 +95,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
override fun onDestroy() { override fun onDestroy() {
equalizerManager.release() equalizerManager.release()
stopWidgetUpdates()
releasePlayer() releasePlayer()
super.onDestroy() super.onDestroy()
} }
@ -182,6 +197,11 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
} else { } else {
MediaManager.scrobble(player.currentMediaItem, false) MediaManager.scrobble(player.currentMediaItem, false)
} }
if (isPlaying) {
scheduleWidgetUpdates()
} else {
stopWidgetUpdates()
}
updateWidget() updateWidget()
} }
@ -231,6 +251,9 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
) )
} }
}) })
if (player.isPlaying) {
scheduleWidgetUpdates()
}
} }
private fun updateWidget() { private fun updateWidget() {
@ -254,6 +277,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
) )
} }
private fun scheduleWidgetUpdates() {
if (widgetUpdateScheduled) return
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
widgetUpdateScheduled = true
}
private fun stopWidgetUpdates() {
if (!widgetUpdateScheduled) return
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
widgetUpdateScheduled = false
}
private fun initializeLoadControl(): DefaultLoadControl { private fun initializeLoadControl(): DefaultLoadControl {
return DefaultLoadControl.Builder() return DefaultLoadControl.Builder()
.setBufferDurationsMs( .setBufferDurationsMs(
@ -316,3 +351,5 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
player.prepare() player.prepare()
} }
} }
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L

View file

@ -6,6 +6,8 @@ import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
import android.os.Binder import android.os.Binder
import android.os.IBinder import android.os.IBinder
import android.os.Handler
import android.os.Looper
import androidx.media3.cast.CastPlayer import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
@ -50,6 +52,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
companion object { companion object {
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER" const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
} }
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
private var widgetUpdateScheduled = false
private val widgetUpdateRunnable = object : Runnable {
override fun run() {
if (!player.isPlaying) {
widgetUpdateScheduled = false
return
}
updateWidget()
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -81,6 +95,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
override fun onDestroy() { override fun onDestroy() {
equalizerManager.release() equalizerManager.release()
stopWidgetUpdates()
releasePlayer() releasePlayer()
super.onDestroy() super.onDestroy()
} }
@ -182,6 +197,11 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
} else { } else {
MediaManager.scrobble(player.currentMediaItem, false) MediaManager.scrobble(player.currentMediaItem, false)
} }
if (isPlaying) {
scheduleWidgetUpdates()
} else {
stopWidgetUpdates()
}
updateWidget() updateWidget()
} }
@ -231,6 +251,9 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
) )
} }
}) })
if (player.isPlaying) {
scheduleWidgetUpdates()
}
} }
private fun updateWidget() { private fun updateWidget() {
@ -253,6 +276,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
) )
} }
private fun scheduleWidgetUpdates() {
if (widgetUpdateScheduled) return
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
widgetUpdateScheduled = true
}
private fun stopWidgetUpdates() {
if (!widgetUpdateScheduled) return
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
widgetUpdateScheduled = false
}
private fun initializeLoadControl(): DefaultLoadControl { private fun initializeLoadControl(): DefaultLoadControl {
return DefaultLoadControl.Builder() return DefaultLoadControl.Builder()
.setBufferDurationsMs( .setBufferDurationsMs(
@ -314,3 +349,5 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
player.prepare() player.prepare()
} }
} }
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L