From 361c55c5acb9fa357801bc447611aa2c418804c4 Mon Sep 17 00:00:00 2001 From: Lauris van Rijn Date: Fri, 29 Aug 2025 00:18:59 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(media):=20remove=20busy=E2=80=91wait=20?= =?UTF-8?q?loop=20in=20library=20personalization=20loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the infinite `while(libraryPersonalizationsDone > 0){}` spin‑loop with an async counter callback. This prevents pegging the CPU if one personalization never completes, and allows completion to trigger via AtomicInteger decrement. Now the final callback fires only when all libraries have finished loading. --- .../audiobookshelf/app/media/MediaManager.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt index 39b3c25f..db3ee4ad 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt @@ -15,6 +15,7 @@ import org.json.JSONException import org.json.JSONObject import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import java.util.concurrent.atomic.AtomicInteger class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) { val tag = "MediaManager" @@ -175,21 +176,20 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) { * Load personalized shelves from server for all libraries. * [cb] resolves when all libraries are processed */ - fun populatePersonalizedDataForAllLibraries(cb: () -> Unit ) { - serverLibraries.forEach { - libraryPersonalizationsDone++ - Log.d(tag, "Loading personalization for library ${it.name} - ${it.id} - ${it.mediaType}") - populatePersonalizedDataForLibrary(it.id) { - Log.d(tag, "Loaded personalization for library ${it.name} - ${it.id} - ${it.mediaType}") - libraryPersonalizationsDone-- + fun populatePersonalizedDataForAllLibraries(cb: () -> Unit) { + val remaining = AtomicInteger(serverLibraries.size) + + serverLibraries.forEach { lib -> + Log.d(tag, "Loading personalization for library ${lib.name}") + populatePersonalizedDataForLibrary(lib.id) { + Log.d(tag, "Loaded personalization for library ${lib.name}") + if (remaining.decrementAndGet() == 0) { + Log.d(tag, "Finished loading all library personalization data") + allLibraryPersonalizationsDone = true + cb() + } } } - - while (libraryPersonalizationsDone > 0) { } - - Log.d(tag, "Finished loading all library personalization data") - allLibraryPersonalizationsDone = true - cb() } /** From 719e517dda851f8feaf3470f21406648f745eae3 Mon Sep 17 00:00:00 2001 From: Lauris van Rijn Date: Fri, 29 Aug 2025 00:19:24 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(androidauto):=20async=20handling=20of?= =?UTF-8?q?=20browseTree=20init=20instead=20of=20busy=E2=80=91loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed blocking `while (!browseTree.isInitialized){}` in `onLoadChildren`. Added `waitForBrowseTree` and `onBrowseTreeInitialized` helpers to queue pending results until browseTree is ready. All browseTree assignments now call `onBrowseTreeInitialized()`. This avoids ANRs and high CPU when Android Auto requests children before init. --- .../app/player/PlayerNotificationService.kt | 64 +++++++++++++++---- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index e37e7147..5e3af81e 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -1087,6 +1087,26 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private val DOWNLOADS_ROOT = "__DOWNLOADS__" private val CONTINUE_ROOT = "__CONTINUE__" private lateinit var browseTree: BrowseTree + private val browseTreeInitListeners = mutableListOf<() -> Unit>() + + private fun waitForBrowseTree(cb: () -> Unit) + { + if (this::browseTree.isInitialized) + { + cb() + } + else + { + browseTreeInitListeners += cb + } + } + + private fun onBrowseTreeInitialized() + { + // Called after browseTree is assigned for the first time + browseTreeInitListeners.forEach { it.invoke() } + browseTreeInitListeners.clear() + } // Only allowing android auto or similar to access media browser service // normal loading of audiobooks is handled in webview (not natively) @@ -1257,6 +1277,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mediaManager.serverLibraries, mediaManager.allLibraryPersonalizationsDone ) + onBrowseTreeInitialized() val children = browseTree[parentMediaId]?.map { item -> Log.d(tag, "Found top menu item: ${item.description.title}") @@ -1291,6 +1312,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mediaManager.serverLibraries, mediaManager.allLibraryPersonalizationsDone ) + onBrowseTreeInitialized() val children = browseTree[parentMediaId]?.map { item -> Log.d(tag, "Found top menu item: ${item.description.title}") @@ -1303,22 +1325,40 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { AbsLogger.info(tag, "onLoadChildren: Android auto data loaded") result.sendResult(children as MutableList?) } - } else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) { + } else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) + { Log.d(tag, "First load done: $firstLoadDone") - if (!firstLoadDone) { + if (!firstLoadDone) + { result.sendResult(null) return } - // Wait until top-menu is initialized - while (!this::browseTree.isInitialized) {} - val children = - browseTree[parentMediaId]?.map { item -> - Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}") - MediaBrowserCompat.MediaItem( - item.description, - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE - ) - } + + if (!this::browseTree.isInitialized) + { + // ✅ good: detach and wait for init + result.detach() + waitForBrowseTree { + val children = browseTree[parentMediaId]?.map { item -> + Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}") + MediaBrowserCompat.MediaItem( + item.description, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } + result.sendResult(children as MutableList?) + } + return + } + + // Already initialized: just return + val children = browseTree[parentMediaId]?.map { item -> + Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}") + MediaBrowserCompat.MediaItem( + item.description, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } result.sendResult(children as MutableList?) } else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library Log.d(tag, "Loading items for library $parentMediaId")