mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-07 20:38:43 +02:00
fix(android-auto): run server connection check off the main thread (#1920)
Android Auto browsing could freeze and crash the app (phone app "closes", Android Auto shows a generic connection error) whenever the Capacitor webview had not yet established the server connection. Root cause: onLoadChildren runs on the media browser service main thread. It calls loadAndroidAutoItems -> checkSetValidServerConnectionConfig, which used runBlocking to ping every saved server config and authorize the user. That blocked the main thread on network I/O. Android Auto (plus Assistant) re-request the browse root every ~2s, and every onGetRoot flags a reload while disconnected, so the blocking work was triggered repeatedly and re-entrantly, producing an ANR. Changes: - checkSetValidServerConnectionConfig now runs on a dedicated Dispatchers.IO scope instead of runBlocking, so pings/authorize never block the caller. The callback contract is unchanged; downstream Android Auto callbacks already ran off the main thread. - loadAndroidAutoItems coalesces overlapping calls into a single in-flight load and fires all queued callbacks on completion, so the browse-root polling storm can no longer spawn concurrent loads racing shared state. - Fix latent bug in checkResetServerItems: server config id was compared with !== (reference identity) instead of != (value), causing spurious cache resets that fed the reload storm. Co-authored-by: databoy2k <18686442+databoy2k@users.noreply.github.com>
This commit is contained in:
@@ -9,8 +9,11 @@ import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.getcapacitor.JSObject
|
||||
import java.util.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import kotlin.coroutines.resume
|
||||
@@ -46,6 +49,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
|
||||
var userSettingsPlaybackRate:Float? = null
|
||||
|
||||
// Android Auto browses on the media browser service main thread, so the server pings
|
||||
// and authorize call must not run there or they block it and trigger an ANR
|
||||
private val androidAutoIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
// Android Auto re-requests the browse root every ~2s and multiple clients do so at once,
|
||||
// so overlapping loads are coalesced into one to avoid racing on the shared server state
|
||||
private var isLoadingAndroidAutoItems = false
|
||||
private val pendingAndroidAutoLoadCallbacks = mutableListOf<() -> Unit>()
|
||||
private val androidAutoLoadLock = Any()
|
||||
|
||||
fun getIsLibrary(id:String) : Boolean {
|
||||
return serverLibraries.find { it.id == id } != null
|
||||
}
|
||||
@@ -136,7 +149,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
// and reset any server data already set
|
||||
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
||||
|
||||
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
||||
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id != serverConfigIdUsed) {
|
||||
podcastEpisodeLibraryItemMap = mutableMapOf()
|
||||
serverLibraries = listOf()
|
||||
serverLibraryItems = mutableListOf()
|
||||
@@ -730,7 +743,10 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
return mediaProgress
|
||||
}
|
||||
|
||||
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
|
||||
// Runs on [androidAutoIOScope] rather than blocking the caller: this is reached from
|
||||
// onLoadChildren on the media browser service main thread, and the pings/authorize below
|
||||
// are network calls, so runBlocking here caused ANRs while browsing in Android Auto
|
||||
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = androidAutoIOScope.launch {
|
||||
Log.d(tag, "checkSetValidServerConnectionConfig | serverConfigIdUsed=$serverConfigIdUsed | lastServerConnectionConfigId=${DeviceManager.deviceData.lastServerConnectionConfigId}")
|
||||
|
||||
coroutineScope {
|
||||
@@ -835,6 +851,26 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
fun loadAndroidAutoItems(cb: () -> Unit) {
|
||||
Log.d(tag, "Load android auto items")
|
||||
|
||||
// Coalesce overlapping calls into a single load, every caller still gets its callback
|
||||
synchronized(androidAutoLoadLock) {
|
||||
pendingAndroidAutoLoadCallbacks.add(cb)
|
||||
if (isLoadingAndroidAutoItems) {
|
||||
Log.d(tag, "loadAndroidAutoItems: Load already in progress, queued callback")
|
||||
return
|
||||
}
|
||||
isLoadingAndroidAutoItems = true
|
||||
}
|
||||
|
||||
val onLoadFinished = {
|
||||
val callbacks: List<() -> Unit>
|
||||
synchronized(androidAutoLoadLock) {
|
||||
isLoadingAndroidAutoItems = false
|
||||
callbacks = pendingAndroidAutoLoadCallbacks.toList()
|
||||
pendingAndroidAutoLoadCallbacks.clear()
|
||||
}
|
||||
callbacks.forEach { it() }
|
||||
}
|
||||
|
||||
// Check if any valid server connection if not use locally downloaded books
|
||||
checkSetValidServerConnectionConfig { isConnected ->
|
||||
if (isConnected) {
|
||||
@@ -844,14 +880,14 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||
loadLibraries { libraries ->
|
||||
if (libraries.isEmpty()) {
|
||||
Log.w(tag, "No libraries returned from server request")
|
||||
cb()
|
||||
onLoadFinished()
|
||||
} else {
|
||||
cb() // Fully loaded
|
||||
onLoadFinished() // Fully loaded
|
||||
}
|
||||
}
|
||||
} else { // Not connected to server
|
||||
Log.d(tag, "loadAndroidAutoItems: Not connected to server")
|
||||
cb()
|
||||
onLoadFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user