Merge branch 'pr-5' into try-pr-5

Fix map loading by drawing a virtual display.
This commit is contained in:
Seb3thehacker committed 2026-09-30 13:04:16 -07:00
commit 473c12ad8f
2 files changed
+47 -7

No files matched your search

@@ -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 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<Int, Int>) -> Unit,
modifier: Modifier,
) {
AndroidView( AndroidView(
modifier = modifier modifier = modifier
.pointerInput(connection) { .pointerInput(connection) {
@@ -114,20 +136,20 @@ private fun AppSurface(
height: Int, height: Int,
) { ) {
GearslipLog.i("stage: surface ready at ${width}x$height") 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) connection.attachSurface(holder.surface, width, height, frame.densityDpi)
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
GearslipLog.i("stage: surface destroyed") 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 @Composable
@@ -139,8 +139,19 @@ class CarAppConnection(private val context: Context) {
// Replaced by another connect (or a disconnect) while waiting on the location service. // Replaced by another connect (or a disconnect) while waiting on the location service.
if (binding !== connection) return@bind if (binding !== connection) return@bind
var bindError: Throwable? = null var bindError: Throwable? = null
val flags = Context.BIND_AUTO_CREATE or LocationKeepAlive.BIND_INCLUDE_CAPABILITIES
val bound = runCatching { 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 { }.getOrElse {
GearslipLog.e("bindService threw for ${app.component}", it) GearslipLog.e("bindService threw for ${app.component}", it)
bindError = it bindError = it
@@ -428,10 +439,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 { main.post {
val current = lent ?: return@post val current = lent ?: return@post
if (surface != null && current.surface !== surface) return@post
lent = null lent = null
sendSurfaceDestroyed(current) sendSurfaceDestroyed(current)
update { it.copy(surfaceAttached = false) } update { it.copy(surfaceAttached = false) }
@@ -714,5 +729,8 @@ class CarAppConnection(private val context: Context) {
private companion object { private companion object {
/** The newest template API this host claims to understand. */ /** The newest template API this host claims to understand. */
const val HOST_API_LEVEL = CarAppApiLevels.LEVEL_8 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
} }
} }