From c6b7f0e1786d2bb6f187b6b874838164c92dcaca Mon Sep 17 00:00:00 2001 From: Seb3thehacker Date: Sun, 4 Oct 2026 13:40:09 -0700 Subject: [PATCH] voice reply keeps listening for cancel, mic asked for in setup The card now closes after no, cancel or no answer instead of popping back up. The setup guide asks for the microphone even with car audio off, and its adb commands use the installed package name. Sensor events are logged once per type plus a count. --- .../seb3thehacker/gearslip/GearslipRunner.kt | 25 +++++++++++- .../seb3thehacker/gearslip/car/VoiceReply.kt | 38 ++++++++++++++----- .../seb3thehacker/gearslip/ui/HelpScreen.kt | 5 ++- .../gearslip/ui/SetupGuideScreen.kt | 15 ++++---- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt index ae1e722..e1dedcf 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/GearslipRunner.kt @@ -451,6 +451,28 @@ class GearslipRunner( openChannel(sensors.serviceId) } + /** + * The car may stream sensors many times a second, on the same thread that reads its audio and + * video messages. Log each type's first reading in full, then only a count every + * [SENSOR_LOG_EVERY_MS], so the log can never hold that thread up. + */ + private val sensorTypesLogged = HashSet() + private var sensorEventsSinceLog = 0 + private var sensorLoggedAt = 0L + + private fun logSensorEvent(body: ByteArray) { + val types = Wire.fields(body).map { it.number }.toSet() + if (sensorTypesLogged.addAll(types)) log.i("<- SensorEvent (first of its kind):\n" + Protobuf.describe(body)) + sensorEventsSinceLog++ + val now = System.currentTimeMillis() + if (sensorLoggedAt == 0L) sensorLoggedAt = now + if (now - sensorLoggedAt >= SENSOR_LOG_EVERY_MS) { + log.i("<- $sensorEventsSinceLog sensor events in the last ${(now - sensorLoggedAt) / 1000}s") + sensorEventsSinceLog = 0 + sensorLoggedAt = now + } + } + private fun onSensorMessage(messageId: Int, body: ByteArray) { when (messageId) { MSG_CHANNEL_OPEN_RESPONSE -> { @@ -477,7 +499,7 @@ class GearslipRunner( MSG_SENSOR_EVENT_INDICATION -> { CarSensors.onEvent(body) - log.i("<- SensorEvent:\n" + Protobuf.describe(body)) + logSensorEvent(body) } // SensorError { required SensorType sensor_type = 1; required SensorErrorType sensor_error_type = 2; } @@ -1000,6 +1022,7 @@ class GearslipRunner( const val MSG_SENSOR_START_RESPONSE = 32770 const val MSG_SENSOR_EVENT_INDICATION = 32771 const val MSG_SENSOR_ERROR = 32772 + private const val SENSOR_LOG_EVERY_MS = 10_000L const val STREAM_MEDIA = 3 const val CODEC_H264_BP = 3 diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/VoiceReply.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/VoiceReply.kt index 70c9dad..d7b9e63 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/car/VoiceReply.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/car/VoiceReply.kt @@ -103,7 +103,7 @@ object VoiceReply { val until = SystemClock.uptimeMillis() + OFFER_MS _state.value = _state.value.copy(until = until, span = OFFER_MS) // The recognizer gives up on its own; this only covers one that doesn't. - main.postAtTime({ if (id == run && _state.value.until != 0L) cancel() }, until + OFFER_GRACE_MS) + main.postAtTime({ if (id == run && _state.value.until != 0L) { CarAssistant.stopListening(); close() } }, until + OFFER_GRACE_MS) CarAssistant.dictate( onPartial = { text -> if (id == run) _state.value = _state.value.copy(heard = text) }, onSpeaking = { @@ -118,11 +118,11 @@ object VoiceReply { _state.value = _state.value.copy(phase = Phase.ASKING, heard = "", until = 0L, span = 0L, held = 0f) CarAssistant.say("Go ahead.") { if (id == run) listen(id) } } else { - finish() + close() } }, - // No answer: the card has already offered Reply and the quick replies all along. - onError = { if (id == run) finish() }, + // No answer counts as no: the message has been heard, so the card goes. + onError = { if (id == run) close() }, ) } } @@ -229,7 +229,7 @@ object VoiceReply { if (id != run) return@dictate when (command(text)) { Command.CANCEL -> { - finish() + close() CarAssistant.say("Not sent.") {} } Command.CHANGE -> { @@ -238,11 +238,21 @@ object VoiceReply { ask(id) } Command.SEND -> send() - // Talk that wasn't meant for us, a passenger or the radio: carry on counting. - null -> startCountdown(id, RESUME_MS) + // Talk that wasn't meant for us, a passenger or the radio: carry on counting, + // and keep listening so a "cancel" after it still lands. + null -> { + startCountdown(id, RESUME_MS) + listenForCommand(id) + } } }, - onError = { if (id == run && _state.value.until == 0L) startCountdown(id, RESUME_MS) }, + // Silence ends the recognizer before the countdown does: listen again until it sends. + onError = { + if (id != run) return@dictate + if (_state.value.until == 0L) startCountdown(id, RESUME_MS) + // A short gap, so a recognizer that fails at once can't spin. + main.postDelayed({ if (id == run) listenForCommand(id) }, RELISTEN_MS) + }, ) } @@ -308,11 +318,20 @@ object VoiceReply { } } - /** Stops wherever it is. Nothing is sent. */ + /** Stops wherever it is. Nothing is sent, and the message card closes. */ fun cancel() { if (!active) return CarAssistant.stopListening() CarAssistant.stopSpeaking() + close() + } + + /** + * Ends without sending and takes the message card with it. Without this the card's own popup + * came back with a fresh bar, as if the message had just arrived. + */ + private fun close() { + _state.value.target?.let { CarNotifications.dismissPopup(it.id) } finish() } @@ -380,4 +399,5 @@ object VoiceReply { private const val OFFER_GRACE_MS = 1_500L /** After talk that wasn't a command, the reply waits this long again before it goes. */ private const val RESUME_MS = 3_000L + private const val RELISTEN_MS = 400L } diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HelpScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HelpScreen.kt index 7cb8595..cf742f9 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HelpScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/HelpScreen.kt @@ -1,5 +1,6 @@ package app.seb3thehacker.gearslip.ui +import app.seb3thehacker.gearslip.BuildConfig import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -61,14 +62,14 @@ private fun setupSteps(onRequestCallScreening: () -> Unit) = listOf( "Connect the phone to a computer with USB debugging turned on, and run this command. " + "It tells Android to trust Gearslip with audio from now on. Skip it and everything " + "still works, but you have to tap Start once each time you set off.", - command = "adb shell appops set app.seb3thehacker.gearslip PROJECT_MEDIA allow", + command = "adb shell appops set ${BuildConfig.APPLICATION_ID} PROJECT_MEDIA allow", ), Step( "Optional: keep notifications readable while music plays.", "Android treats sending audio to the car like sharing your screen, and hides what your " + "notifications say. Run this command too, and Gearslip switches that off only while " + "music is going to the car, then puts it back.", - command = "adb shell pm grant app.seb3thehacker.gearslip android.permission.WRITE_SECURE_SETTINGS", + command = "adb shell pm grant ${BuildConfig.APPLICATION_ID} android.permission.WRITE_SECURE_SETTINGS", ), Step( "Optional: let the car screen answer and decline calls.", diff --git a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt index 628dc0b..68cd95f 100644 --- a/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt +++ b/gearslip/src/main/java/app/seb3thehacker/gearslip/ui/SetupGuideScreen.kt @@ -1,5 +1,6 @@ package app.seb3thehacker.gearslip.ui +import app.seb3thehacker.gearslip.BuildConfig import android.Manifest import android.app.AppOpsManager import android.app.NotificationManager @@ -43,7 +44,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import app.seb3thehacker.gearslip.AppSettings import app.seb3thehacker.gearslip.call.CarCalls -import app.seb3thehacker.gearslip.car.CarSettings import app.seb3thehacker.gearslip.car.Prefetch import app.seb3thehacker.gearslip.host.AndroidAuto import app.seb3thehacker.gearslip.notify.GearslipNotificationListener @@ -104,13 +104,14 @@ private fun pages(context: Context, onRequestCallScreening: () -> Unit): List Unit): List Unit): List