mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-29 22:57:16 +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.audiobookshelf.app.server.ApiHandler
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
@@ -46,6 +49,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
|
|
||||||
var userSettingsPlaybackRate:Float? = null
|
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 {
|
fun getIsLibrary(id:String) : Boolean {
|
||||||
return serverLibraries.find { it.id == id } != null
|
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
|
// and reset any server data already set
|
||||||
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
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()
|
podcastEpisodeLibraryItemMap = mutableMapOf()
|
||||||
serverLibraries = listOf()
|
serverLibraries = listOf()
|
||||||
serverLibraryItems = mutableListOf()
|
serverLibraryItems = mutableListOf()
|
||||||
@@ -730,7 +743,10 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return mediaProgress
|
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}")
|
Log.d(tag, "checkSetValidServerConnectionConfig | serverConfigIdUsed=$serverConfigIdUsed | lastServerConnectionConfigId=${DeviceManager.deviceData.lastServerConnectionConfigId}")
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
@@ -835,6 +851,26 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
fun loadAndroidAutoItems(cb: () -> Unit) {
|
fun loadAndroidAutoItems(cb: () -> Unit) {
|
||||||
Log.d(tag, "Load android auto items")
|
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
|
// Check if any valid server connection if not use locally downloaded books
|
||||||
checkSetValidServerConnectionConfig { isConnected ->
|
checkSetValidServerConnectionConfig { isConnected ->
|
||||||
if (isConnected) {
|
if (isConnected) {
|
||||||
@@ -844,14 +880,14 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
loadLibraries { libraries ->
|
loadLibraries { libraries ->
|
||||||
if (libraries.isEmpty()) {
|
if (libraries.isEmpty()) {
|
||||||
Log.w(tag, "No libraries returned from server request")
|
Log.w(tag, "No libraries returned from server request")
|
||||||
cb()
|
onLoadFinished()
|
||||||
} else {
|
} else {
|
||||||
cb() // Fully loaded
|
onLoadFinished() // Fully loaded
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Not connected to server
|
} else { // Not connected to server
|
||||||
Log.d(tag, "loadAndroidAutoItems: Not connected to server")
|
Log.d(tag, "loadAndroidAutoItems: Not connected to server")
|
||||||
cb()
|
onLoadFinished()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user