From 327d6fd40a8b9dba21d2622faf818d71492306a7 Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Tue, 27 Oct 2026 14:12:02 -0700 Subject: [PATCH] protocol: service discovery sends Gearslip's icon at three sizes and a session ID, like a real phone --- .../gearslip/GearslipActivity.kt | 2 ++ .../seb3thehacker/gearslip/GearslipRunner.kt | 17 ++++++++-- .../app/seb3thehacker/gearslip/PhoneIcons.kt | 34 +++++++++++++++++++ 3 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 gearslip/src/main/java/app/seb3thehacker/gearslip/PhoneIcons.kt diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt index a397a1f..4a45b43 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipActivity.kt @@ -401,6 +401,7 @@ class GearslipActivity : ComponentActivity(), Projection { identityProvider = { CertProvider.load(this) }, projection = this, vehicleProfileFor = { info -> VehicleProfiles.find(VehicleProfiles.load(this), info) }, + phoneIcons = { PhoneIcons.pngs(this) }, ) this.runner = runner worker = Thread { runner.run() }.apply { name = "gearslip"; start() } @@ -423,6 +424,7 @@ class GearslipActivity : ComponentActivity(), Projection { projection = this, vehicleProfileFor = { info -> VehicleProfiles.find(VehicleProfiles.load(this), info) }, wireless = true, + phoneIcons = { PhoneIcons.pngs(this) }, ) this.runner = runner worker = Thread { runner.run() }.apply { name = "gearslip"; start() } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt index 89ac915..37b4f5c 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt @@ -48,6 +48,8 @@ class GearslipRunner( private val vehicleProfileFor: (ServiceDiscovery.HeadUnitInfo) -> VehicleProfile? = { null }, /** Over Wi-Fi rather than USB: the video is sent smaller so the link keeps up. */ private val wireless: Boolean = false, + /** Gearslip's icon as 32, 64 and 128 px PNGs, for the car's phone tile. See [PhoneIcons]. */ + private val phoneIcons: () -> List = { emptyList() }, ) { private val log = GearslipLog.tagged("PROTO") @@ -446,11 +448,20 @@ class GearslipRunner( if (authenticationFailed) running = false } + /** + * What a real phone sends here: its icon at three sizes (fields 1 to 3), a label, its model, + * and a session ID that's new each connection (field 6). A 2017 Hyundai on protocol 1.0 + * never answered the request when it had only the label and model (issue #47). + */ private fun sendServiceDiscoveryRequest() { - val body = Protobuf.stringField(4, "Gearslip") + // label_text - Protobuf.stringField(5, Build.MODEL) // device_name + val icons = runCatching(phoneIcons).getOrDefault(emptyList()) + var body = ByteArray(0) + icons.take(3).forEachIndexed { i, png -> body += Protobuf.bytesField(i + 1, png) } // small, medium, large icon + body += Protobuf.stringField(4, "Gearslip") + // label_text + Protobuf.stringField(5, Build.MODEL) + // device_name + Protobuf.bytesField(6, Protobuf.stringField(1, java.util.UUID.randomUUID().toString())) // session ID send(MSG_SERVICE_DISCOVERY_REQUEST, body, encrypted = true) - log.i("-> ServiceDiscoveryRequest (encrypted)") + log.i("-> ServiceDiscoveryRequest (encrypted, ${icons.size} icons, ${body.size} bytes)") } private fun onServiceDiscoveryResponse(body: ByteArray) { diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/PhoneIcons.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/PhoneIcons.kt new file mode 100644 index 0000000..147ee46 --- /dev/null +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/PhoneIcons.kt @@ -0,0 +1,34 @@ +package app.seb3thehacker.gearslip + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import java.io.ByteArrayOutputStream + +/** + * Gearslip's own icon as the PNGs a phone hands the car in its ServiceDiscoveryRequest, at the + * 32, 64 and 128 px a real phone sends. Cars draw the phone's tile on their home screen from + * them, and older head units may wait for them before answering at all. + */ +object PhoneIcons { + + private val SIZES = intArrayOf(32, 64, 128) + + @Volatile private var cached: List? = null + + /** The three PNGs, smallest first; empty if the icon can't be drawn. */ + fun pngs(context: Context): List = cached ?: runCatching { + val icon = context.packageManager.getApplicationIcon(context.applicationInfo).mutate() + SIZES.map { size -> + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + icon.setBounds(0, 0, size, size) + icon.draw(Canvas(bitmap)) + val png = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 100, png) + bitmap.recycle() + png.toByteArray() + } + }.onFailure { GearslipLog.w("phone icons: couldn't draw them (${it.message})") } + .getOrDefault(emptyList()) + .also { if (it.isNotEmpty()) cached = it } +}