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 } }