Skip to content

Commit 1ad745f

Browse files
malysclaude
andcommitted
fix: the speak action uses the car's voice, so it works again
The head unit has no Android TTS engine. It still talks — through the SAIC voice service — so Speaker tries that first and keeps the platform engine as the fallback for a bench emulator. It is also the cheaper path: one binder call, no engine to spin up, no audio focus, nothing to block on. SpeechEngines.any() now counts the vehicle voice, which is what stops the editor hiding an action the car can perform, and the Diagnostic TTS row names it among the engines found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 63dc4d1 commit 1ad745f

5 files changed

Lines changed: 42 additions & 9 deletions

File tree

‎MG4Hardware‎

‎app/src/main/java/com/mg4/tasker/debug/DiagnosticProbe.kt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ object DiagnosticProbe {
117117
val bridge = ProfileBridge(appContext)
118118
val bridgeReachable = try { bridge.connect() } finally { bridge.disconnect() }
119119

120-
val engines = SpeechEngines.list(appContext)
120+
val engines = SpeechEngines.describe(appContext)
121121
val caps = Diagnostics.Capabilities(
122122
vehicleLayerReady = MG4Hardware.isCarPropertyManagerReady(),
123123
gateVerdict = gateVerdict(),

‎app/src/main/java/com/mg4/tasker/util/Speaker.kt‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,28 @@ import android.media.AudioManager
77
import android.speech.tts.TextToSpeech
88
import android.speech.tts.UtteranceProgressListener
99
import com.mg4.hardware.AppLogger
10+
import com.mg4.hardware.saic.SaicTts
1011
import java.util.Locale
1112
import java.util.concurrent.CountDownLatch
1213
import java.util.concurrent.TimeUnit
1314

1415
/**
15-
* Speaks a rule message through the platform TTS engine.
16+
* Speaks a rule message.
1617
*
17-
* The engine is created per utterance and released once it has spoken: rules fire a few
18-
* times per drive at most, and a resident engine would hold an audio focus client alive
19-
* for the whole ignition cycle for nothing.
18+
* **The car's own voice first.** The head unit talks — it announces tyre faults and
19+
* navigation — but through a SAIC vendor service, not through `android.speech.tts`. Probing
20+
* for a platform engine and finding none was the right answer to the wrong question: the
21+
* vehicle has no Android TTS engine installed and does not need one. [SaicTts] is therefore
22+
* tried first, and it is also the cheaper path — a one-way binder call, no engine to spin
23+
* up, no audio focus to take, nothing to block on.
2024
*
21-
* ⚠️ Blocks until the engine has spoken (or the timeout expires); call off the main
22-
* thread — which is where the rule engine already runs.
25+
* The platform engine stays as the fallback, for a bench emulator or a head unit whose
26+
* vendor service is absent. There it is created per utterance and released once it has
27+
* spoken: rules fire a few times per drive at most, and a resident engine would hold an
28+
* audio focus client alive for the whole ignition cycle for nothing.
29+
*
30+
* ⚠️ The fallback blocks until the engine has spoken (or the timeout expires); call off the
31+
* main thread — which is where the rule engine already runs.
2332
*/
2433
object Speaker {
2534

@@ -59,6 +68,12 @@ object Speaker {
5968
fun speak(context: Context, message: String): Failure? {
6069
if (message.isBlank()) return Failure.EMPTY_MESSAGE
6170

71+
// Queued, not interrupting: a tyre warning outranks "profile applied".
72+
if (SaicTts.isAvailable && SaicTts.speak(message)) {
73+
AppLogger.d(TAG, "spoken by the vehicle voice service")
74+
return null
75+
}
76+
6277
val appContext = context.applicationContext
6378
val ready = CountDownLatch(1)
6479
var status = TextToSpeech.ERROR

‎app/src/main/java/com/mg4/tasker/util/SpeechEngines.kt‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import android.content.Intent
55
import android.provider.Settings
66
import android.speech.tts.TextToSpeech
7+
import com.mg4.hardware.saic.SaicTts
78

89
/**
910
* Which text-to-speech engines this app can see on the head unit.
@@ -39,5 +40,18 @@ object SpeechEngines {
3940
return listOfNotNull(default?.takeIf { it.isNotBlank() })
4041
}
4142

42-
fun any(context: Context): Boolean = list(context).isNotEmpty()
43+
/**
44+
* Whether anything can speak — the vehicle's own voice counts.
45+
*
46+
* [list] only knows about Android TTS engines, and the MG4 has none: it talks through a
47+
* SAIC vendor service. Answering "no engine" there hid the speak action from the editor
48+
* on a car that speaks perfectly well, which is the bug this distinction exists to fix.
49+
*/
50+
fun any(context: Context): Boolean = SaicTts.isAvailable || list(context).isNotEmpty()
51+
52+
/** Engine names for the report, the vehicle voice service included when it is bound. */
53+
fun describe(context: Context): List<String> =
54+
(if (SaicTts.isAvailable) listOf(VEHICLE_VOICE) else emptyList()) + list(context)
55+
56+
const val VEHICLE_VOICE = "com.saicmotor.voicetts (vehicle)"
4357
}

‎app/src/main/java/com/mg4/tasker/vehicle/VendorServices.kt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import android.content.Context
44
import com.mg4.hardware.saic.SaicHub
55
import com.mg4.hardware.saic.SaicPhone
66
import com.mg4.hardware.saic.SaicRadio
7+
import com.mg4.hardware.saic.SaicTts
78

89
/**
910
* Binds the SAIC vendor services the catalogue depends on.
@@ -21,5 +22,8 @@ object VendorServices {
2122
SaicHub.connect(appContext)
2223
SaicRadio.connect(appContext)
2324
SaicPhone.connect(appContext)
25+
// The vehicle's own voice — what the "speak" action uses in preference to an Android
26+
// TTS engine, which this head unit does not have.
27+
SaicTts.connect(appContext)
2428
}
2529
}

0 commit comments

Comments
 (0)