From 5250adb36e8961ab54a123108e574050abad7a14 Mon Sep 17 00:00:00 2001 From: Benjamin Porter Date: Sun, 4 Jan 2026 10:03:50 -0700 Subject: [PATCH 1/2] Don't queue display events while backgrounded When we are backgrounded, don't queue display events. This prevents a hang and sluggish replay of display events once we are re-foregrounded later --- .../app/managers/SleepTimerManager.kt | 14 ++++++++++ .../app/plugins/AbsAudioPlayer.kt | 27 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt index eb2e51ac..ca359edc 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/SleepTimerManager.kt @@ -527,4 +527,18 @@ constructor(private val playerNotificationService: PlayerNotificationService) { checkAutoSleepTimer() } + + /** + * Sends the current sleep timer state to the client. + * Called when app resumes from background to sync UI state. + */ + fun sendCurrentSleepTimerState() { + if (sleepTimerRunning) { + val timeRemaining = getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()) + playerNotificationService.clientEventEmitter?.onSleepTimerSet(timeRemaining, isAutoSleepTimer) + } else { + // No timer running - send 0 to clear any stale UI state + playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false) + } + } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index 79a460d1..a5f1935e 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -33,6 +33,9 @@ class AbsAudioPlayer : Plugin() { private var isCastAvailable:Boolean = false + // Track foreground state to avoid flooding WebView with events while backgrounded + private var isInForeground: Boolean = true + override fun load() { mainActivity = (activity as MainActivity) apiHandler = ApiHandler(mainActivity) @@ -60,6 +63,8 @@ class AbsAudioPlayer : Plugin() { } override fun onMetadata(metadata: PlaybackMetadata) { + // Skip frequent metadata updates when app is backgrounded to prevent event queue buildup + if (!isInForeground) return notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata))) } @@ -68,6 +73,8 @@ class AbsAudioPlayer : Plugin() { } override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) { + // Skip sleep timer updates when app is backgrounded to prevent event queue buildup + if (!isInForeground) return val ret = JSObject() ret.put("value", sleepTimeRemaining) ret.put("isAuto", isAutoSleepTimer) @@ -118,6 +125,26 @@ class AbsAudioPlayer : Plugin() { notifyListeners(evtName, ret) } + override fun handleOnPause() { + super.handleOnPause() + isInForeground = false + Log.d(tag, "App paused - disabling frequent event emission") + } + + override fun handleOnResume() { + super.handleOnResume() + isInForeground = true + Log.d(tag, "App resumed - enabling event emission and sending current state") + + // Send current state to UI after resume to sync up + if (::playerNotificationService.isInitialized && playerNotificationService.currentPlaybackSession != null) { + Handler(Looper.getMainLooper()).post { + playerNotificationService.sendClientMetadata(PlayerState.READY) + playerNotificationService.sleepTimerManager.sendCurrentSleepTimerState() + } + } + } + private fun initCastManager() { val googleApi = GoogleApiAvailability.getInstance() val statusCode = googleApi.isGooglePlayServicesAvailable(mainActivity) From 140bc7fbc5c29f4c725f4e193c713c1d26c8c1a8 Mon Sep 17 00:00:00 2001 From: Benjamin Porter Date: Mon, 5 Jan 2026 16:58:10 -0700 Subject: [PATCH 2/2] Skip progress updates when backgrounded When we are backgrounded, these queue events build up and can cause performance issues as the queue is processed. We don't need to process these when backgrounded, so skip them. Also add small delay when resuming for webview to load before sending it update events --- .../audiobookshelf/app/plugins/AbsAudioPlayer.kt | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index a5f1935e..41d7cbf7 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -82,6 +82,8 @@ class AbsAudioPlayer : Plugin() { } override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) { + // Skip progress updates when app is backgrounded to prevent event queue buildup + if (!isInForeground) return notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) } @@ -128,20 +130,21 @@ class AbsAudioPlayer : Plugin() { override fun handleOnPause() { super.handleOnPause() isInForeground = false - Log.d(tag, "App paused - disabling frequent event emission") } override fun handleOnResume() { super.handleOnResume() isInForeground = true - Log.d(tag, "App resumed - enabling event emission and sending current state") - // Send current state to UI after resume to sync up + // Send current state to UI after resume to sync up (with small delay to let WebView fully resume) if (::playerNotificationService.isInitialized && playerNotificationService.currentPlaybackSession != null) { - Handler(Looper.getMainLooper()).post { + Handler(Looper.getMainLooper()).postDelayed({ playerNotificationService.sendClientMetadata(PlayerState.READY) playerNotificationService.sleepTimerManager.sendCurrentSleepTimerState() - } + playerNotificationService.mediaProgressSyncer.currentLocalMediaProgress?.let { + playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it) + } + }, 100) } }