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