From 3daed77ba9161e66e86c98846bebfdcbcfb5244a Mon Sep 17 00:00:00 2001 From: Ayaan Zaidi Date: Sat, 28 Feb 2026 19:33:42 +0530 Subject: [PATCH] fix(android): unify voice speaker gating and config refresh --- .../java/ai/openclaw/android/NodeRuntime.kt | 22 ++++++++++----- .../openclaw/android/voice/TalkModeManager.kt | 27 +++++++++++++++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt index 640821f98da..f462413669b 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/NodeRuntime.kt @@ -249,7 +249,12 @@ class NodeRuntime(context: Context) { applyMainSessionKey(mainSessionKey) updateStatus() micCapture.onGatewayConnectionChanged(true) - scope.launch { refreshBrandingFromGateway() } + scope.launch { + refreshBrandingFromGateway() + if (voiceReplySpeakerLazy.isInitialized()) { + voiceReplySpeaker.refreshConfig() + } + } }, onDisconnected = { message -> operatorConnected = false @@ -319,7 +324,7 @@ class NodeRuntime(context: Context) { json = json, supportsChatSubscribe = false, ) - private val voiceReplySpeaker: TalkModeManager by lazy { + private val voiceReplySpeakerLazy: Lazy = lazy { // Reuse the existing TalkMode speech engine (ElevenLabs + deterministic system-TTS fallback) // without enabling the legacy talk capture loop. TalkModeManager( @@ -328,8 +333,12 @@ class NodeRuntime(context: Context) { session = operatorSession, supportsChatSubscribe = false, isConnected = { operatorConnected }, - ) + ).also { speaker -> + speaker.setPlaybackEnabled(prefs.speakerEnabled.value) + } } + private val voiceReplySpeaker: TalkModeManager + get() = voiceReplySpeakerLazy.value private val micCapture: MicCaptureManager by lazy { MicCaptureManager( @@ -349,9 +358,7 @@ class NodeRuntime(context: Context) { parseChatSendRunId(response) ?: idempotencyKey }, speakAssistantReply = { text -> - if (prefs.speakerEnabled.value) { - voiceReplySpeaker.speakAssistantReply(text) - } + voiceReplySpeaker.speakAssistantReply(text) }, ) } @@ -641,6 +648,9 @@ class NodeRuntime(context: Context) { fun setSpeakerEnabled(value: Boolean) { prefs.setSpeakerEnabled(value) + if (voiceReplySpeakerLazy.isInitialized()) { + voiceReplySpeaker.setPlaybackEnabled(value) + } } fun refreshGatewayConnection() { diff --git a/apps/android/app/src/main/java/ai/openclaw/android/voice/TalkModeManager.kt b/apps/android/app/src/main/java/ai/openclaw/android/voice/TalkModeManager.kt index 71d727a5890..cd978ba8314 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/voice/TalkModeManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/voice/TalkModeManager.kt @@ -146,6 +146,8 @@ class TalkModeManager( private var pendingRunId: String? = null private var pendingFinal: CompletableDeferred? = null private var chatSubscribedSessionKey: String? = null + private var configLoaded = false + @Volatile private var playbackEnabled = true private var player: MediaPlayer? = null private var streamingSource: StreamingMediaDataSource? = null @@ -194,8 +196,21 @@ class TalkModeManager( } } - suspend fun speakAssistantReply(text: String) { + fun setPlaybackEnabled(enabled: Boolean) { + playbackEnabled = enabled + if (!enabled) { + stopSpeaking() + } + } + + suspend fun refreshConfig() { reloadConfig() + } + + suspend fun speakAssistantReply(text: String) { + if (!playbackEnabled) return + ensureConfigLoaded() + if (!playbackEnabled) return playAssistant(text) } @@ -347,7 +362,7 @@ class TalkModeManager( lastTranscript = "" lastHeardAtMs = null - reloadConfig() + ensureConfigLoaded() val prompt = buildPrompt(transcript) if (!isConnected()) { _statusText.value = "Gateway not connected" @@ -855,6 +870,12 @@ class TalkModeManager( return true } + private suspend fun ensureConfigLoaded() { + if (!configLoaded) { + reloadConfig() + } + } + private suspend fun reloadConfig() { val envVoice = System.getenv("ELEVENLABS_VOICE_ID")?.trim() val sagVoice = System.getenv("SAG_VOICE_ID")?.trim() @@ -907,6 +928,7 @@ class TalkModeManager( } else if (selection?.normalizedPayload == true) { Log.d(tag, "talk config provider=elevenlabs") } + configLoaded = true } catch (_: Throwable) { defaultVoiceId = envVoice?.takeIf { it.isNotEmpty() } ?: sagVoice?.takeIf { it.isNotEmpty() } defaultModelId = defaultModelIdFallback @@ -914,6 +936,7 @@ class TalkModeManager( apiKey = envKey?.takeIf { it.isNotEmpty() } voiceAliases = emptyMap() defaultOutputFormat = defaultOutputFormatFallback + configLoaded = true } }