From 6156da4b670a70b2bf6a2461e3cd82ba307b6451 Mon Sep 17 00:00:00 2001 From: Brian5647 <3bpdzn3s@bserver.eu.org> Date: Wed, 30 Sep 2026 20:29:58 +0200 Subject: [PATCH] Keep car apps online in the background and fix surface resizes Car apps are now bound as a foreground service, so they keep network access when Gearslip isn't the top app on the phone. Before, an app bound in the background could lose network under Doze: its map and GPS kept working while search and routing went dead. The app surface is also recreated whenever the map area changes size, the way Android Auto does it, instead of being resized in place. Apps that draw through their own VirtualDisplay rebuild it on the reused surface, and the old display's teardown then cuts the new one off, freezing the map for good. Found with Flitsmeister after picking a route. Co-Authored-By: Claude Opus 5.5 --- .../seb3thehacker/gearslip/car/CarAppStage.kt | 30 ++++++++++++++++--- .../gearslip/host/CarAppConnection.kt | 24 +++++++++++++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarAppStage.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarAppStage.kt index af4a949..82e2bdc 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarAppStage.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/CarAppStage.kt @@ -89,6 +89,28 @@ private fun AppSurface( } } + // A fresh SurfaceView - and so a fresh Surface - for every size, the way Android Auto hands an + // app a new surface rather than resizing the old one. A resized SurfaceView keeps the same + // BufferQueue, and apps that draw through their own VirtualDisplay (Flitsmeister) build the + // new one on it before releasing the old: the old display's disconnect then takes the new one's + // connection with it and the map freezes for good. The nav bar hiding for the search keyboard + // and the lyrics pane opening both resize this area. + androidx.compose.foundation.layout.BoxWithConstraints(modifier) { + androidx.compose.runtime.key(constraints.maxWidth, constraints.maxHeight) { + SurfaceHost(connection, frame, onSize = { size = it }, Modifier.fillMaxSize()) + } + } + + DisposableEffect(connection) { onDispose { connection.detachSurface() } } +} + +@Composable +private fun SurfaceHost( + connection: CarAppConnection, + frame: CarEnvironment.Frame, + onSize: (Pair) -> Unit, + modifier: Modifier, +) { AndroidView( modifier = modifier .pointerInput(connection) { @@ -114,20 +136,20 @@ private fun AppSurface( height: Int, ) { GearslipLog.i("stage: surface ready at ${width}x$height") - size = width to height + onSize(width to height) connection.attachSurface(holder.surface, width, height, frame.densityDpi) } override fun surfaceDestroyed(holder: SurfaceHolder) { GearslipLog.i("stage: surface destroyed") - connection.detachSurface() + // Only this view's own surface: a replaced view's teardown can land after + // its successor has already lent the app a new one. + connection.detachSurface(holder.surface) } }) } }, ) - - DisposableEffect(connection) { onDispose { connection.detachSurface() } } } @Composable diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/host/CarAppConnection.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/host/CarAppConnection.kt index 9217903..b6b478d 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/host/CarAppConnection.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/host/CarAppConnection.kt @@ -136,8 +136,19 @@ class CarAppConnection(private val context: Context) { LocationKeepAlive.start(context) val bind = { var bindError: Throwable? = null + val flags = Context.BIND_AUTO_CREATE or LocationKeepAlive.BIND_INCLUDE_CAPABILITIES val bound = runCatching { - context.bindService(intent, connection, Context.BIND_AUTO_CREATE or LocationKeepAlive.BIND_INCLUDE_CAPABILITIES) + // Without BIND_FOREGROUND_SERVICE, an app bound by a foreground service (which is + // all Gearslip is once the phone screen leaves it) only reaches "important + // foreground", and that state loses network under Doze and background network + // restrictions: the map and GPS keep working while search and routing go dead. + // In the preview Gearslip is the top app, which hides this. Retried without the + // flag in case a build of Android refuses it from an unprivileged caller. + runCatching { context.bindService(intent, connection, flags or BIND_FOREGROUND_SERVICE) } + .getOrElse { + GearslipLog.w("host: foreground binding refused (${it.message}); binding without it") + context.bindService(intent, connection, flags) + } }.getOrElse { GearslipLog.e("bindService threw for ${app.component}", it) bindError = it @@ -409,10 +420,14 @@ class CarAppConnection(private val context: Context) { } } - /** Takes the Surface back, e.g. when the SurfaceView goes away. */ - fun detachSurface() { + /** + * Takes the Surface back, e.g. when the SurfaceView goes away. With [surface], only if that is + * still the one lent - a replaced view's teardown must not take back its successor's. + */ + fun detachSurface(surface: Surface? = null) { main.post { val current = lent ?: return@post + if (surface != null && current.surface !== surface) return@post lent = null sendSurfaceDestroyed(current) update { it.copy(surfaceAttached = false) } @@ -695,5 +710,8 @@ class CarAppConnection(private val context: Context) { private companion object { /** The newest template API this host claims to understand. */ const val HOST_API_LEVEL = CarAppApiLevels.LEVEL_8 + + /** Context.BIND_FOREGROUND_SERVICE; the constant itself is hidden from the SDK. */ + const val BIND_FOREGROUND_SERVICE = 0x04000000 } }