mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-17 03:21:38 +00:00
fix(android): refresh retained voice and chat text
This commit is contained in:
@@ -6,7 +6,10 @@ import ai.openclaw.app.gateway.GatewayRequestNotEnqueued
|
||||
import ai.openclaw.app.gateway.GatewayRequestOutcomeUnknown
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.gateway.parseChatSendAck
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.i18n.NativeText
|
||||
import ai.openclaw.app.i18n.nativeText
|
||||
import ai.openclaw.app.i18n.resolveOptionalNativeText
|
||||
import ai.openclaw.app.i18n.verbatimText
|
||||
import ai.openclaw.app.parseGatewayModels
|
||||
import ai.openclaw.app.resolveAgentIdFromMainSessionKey
|
||||
import ai.openclaw.app.ui.chat.thinkingSupportedForSelection
|
||||
@@ -120,8 +123,8 @@ class ChatController internal constructor(
|
||||
private val _historyLoading = MutableStateFlow(false)
|
||||
val historyLoading: StateFlow<Boolean> = _historyLoading.asStateFlow()
|
||||
|
||||
private val _errorText = MutableStateFlow<String?>(null)
|
||||
val errorText: StateFlow<String?> = _errorText.asStateFlow()
|
||||
private val _errorText = MutableStateFlow<NativeText?>(null)
|
||||
val errorText: StateFlow<String?> = _errorText.resolveOptionalNativeText()
|
||||
|
||||
private val _healthOk = MutableStateFlow(false)
|
||||
val healthOk: StateFlow<Boolean> = _healthOk.asStateFlow()
|
||||
@@ -209,6 +212,14 @@ class ChatController internal constructor(
|
||||
private fun updateErrorText(
|
||||
message: String?,
|
||||
historyGeneration: Long? = null,
|
||||
) {
|
||||
_errorText.value = message?.let(::verbatimText)
|
||||
historyLoadErrorGeneration = historyGeneration
|
||||
}
|
||||
|
||||
private fun updateLocalizedErrorText(
|
||||
message: NativeText?,
|
||||
historyGeneration: Long? = null,
|
||||
) {
|
||||
_errorText.value = message
|
||||
historyLoadErrorGeneration = historyGeneration
|
||||
@@ -702,7 +713,7 @@ class ChatController internal constructor(
|
||||
val parentKey = normalizeRequestedSessionKey(_sessionKey.value)
|
||||
if (parentKey.isEmpty()) return false
|
||||
if (_pendingRunCount.value > 0) {
|
||||
updateErrorText(nativeString("Wait for the current response to finish before starting a new chat."))
|
||||
updateLocalizedErrorText(nativeText("Wait for the current response to finish before starting a new chat."))
|
||||
return false
|
||||
}
|
||||
if (!newChatCreateInFlight.compareAndSet(false, true)) {
|
||||
@@ -799,7 +810,7 @@ class ChatController internal constructor(
|
||||
} catch (err: CancellationException) {
|
||||
throw err
|
||||
} catch (err: Throwable) {
|
||||
updateErrorText(err.message ?: nativeString("Could not update model."))
|
||||
updateLocalizedErrorText(err.message?.let(::verbatimText) ?: nativeText("Could not update model."))
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -947,7 +958,7 @@ class ChatController internal constructor(
|
||||
when (val outbox = commandOutbox) {
|
||||
null -> {
|
||||
if (!_healthOk.value) {
|
||||
updateErrorText(nativeString("Gateway health not OK; cannot send"))
|
||||
updateLocalizedErrorText(nativeText("Gateway health not OK; cannot send"))
|
||||
return false
|
||||
}
|
||||
null
|
||||
@@ -1078,7 +1089,7 @@ class ChatController internal constructor(
|
||||
// Terminal timeout/error means the gateway did not accept a runnable turn.
|
||||
// Surface failed acceptance instead of letting a cleared composer look successful.
|
||||
unresolvedRepliesByRunId.remove(actualRunId)
|
||||
updateErrorText(nativeString("Chat failed before the run started; try again."))
|
||||
updateLocalizedErrorText(nativeText("Chat failed before the run started; try again."))
|
||||
// The parked row owns the input; restoring the draft would duplicate it.
|
||||
journaled != null
|
||||
}
|
||||
@@ -1821,7 +1832,7 @@ class ChatController internal constructor(
|
||||
attachments: List<OutgoingAttachment>,
|
||||
): ChatOutboxItem? {
|
||||
if (outboxScope == null) {
|
||||
updateErrorText(nativeString("Gateway health not OK; cannot send"))
|
||||
updateLocalizedErrorText(nativeText("Gateway health not OK; cannot send"))
|
||||
return null
|
||||
}
|
||||
val payloads =
|
||||
@@ -1836,7 +1847,7 @@ class ChatController internal constructor(
|
||||
)
|
||||
}
|
||||
} catch (_: IllegalArgumentException) {
|
||||
updateErrorText(nativeString("Could not stage an attachment for sending."))
|
||||
updateLocalizedErrorText(nativeText("Could not stage an attachment for sending."))
|
||||
return null
|
||||
}
|
||||
// Slash commands are connection-gated: they may auto-send only inside the connection epoch
|
||||
@@ -1856,7 +1867,7 @@ class ChatController internal constructor(
|
||||
} catch (err: CancellationException) {
|
||||
throw err
|
||||
} catch (_: Throwable) {
|
||||
updateErrorText(nativeString("Could not queue message for later delivery."))
|
||||
updateLocalizedErrorText(nativeText("Could not queue message for later delivery."))
|
||||
return null
|
||||
}
|
||||
return when (result) {
|
||||
@@ -1866,19 +1877,19 @@ class ChatController internal constructor(
|
||||
result.item
|
||||
}
|
||||
ChatOutboxEnqueueResult.QueueFull -> {
|
||||
updateErrorText(nativeString("Offline queue is full (\$OUTBOX_MAX_QUEUED messages); delete queued items first.", OUTBOX_MAX_QUEUED))
|
||||
updateLocalizedErrorText(nativeText("Offline queue is full (\$OUTBOX_MAX_QUEUED messages); delete queued items first.", OUTBOX_MAX_QUEUED))
|
||||
null
|
||||
}
|
||||
ChatOutboxEnqueueResult.AttachmentsTooLarge -> {
|
||||
updateErrorText(nativeString("Attachments are too large to queue for one message; remove some and try again."))
|
||||
updateLocalizedErrorText(nativeText("Attachments are too large to queue for one message; remove some and try again."))
|
||||
null
|
||||
}
|
||||
ChatOutboxEnqueueResult.StorageFull -> {
|
||||
updateErrorText(nativeString("Offline attachment storage is full; delete queued items first."))
|
||||
updateLocalizedErrorText(nativeText("Offline attachment storage is full; delete queued items first."))
|
||||
null
|
||||
}
|
||||
ChatOutboxEnqueueResult.Unavailable -> {
|
||||
updateErrorText(nativeString("Gateway health not OK; cannot send"))
|
||||
updateLocalizedErrorText(nativeText("Gateway health not OK; cannot send"))
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -2484,7 +2495,13 @@ class ChatController internal constructor(
|
||||
pendingToolCallsById.clear()
|
||||
publishPendingToolCalls()
|
||||
_streamingAssistantText.value = null
|
||||
updateErrorText(if (state == "error") payload["errorMessage"].asStringOrNull() ?: nativeString("Chat failed") else null)
|
||||
updateLocalizedErrorText(
|
||||
if (state == "error") {
|
||||
payload["errorMessage"].asStringOrNull()?.let(::verbatimText) ?: nativeText("Chat failed")
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
refreshCurrentHistoryBestEffort(updateSessionInfo = true)
|
||||
return
|
||||
@@ -2498,7 +2515,7 @@ class ChatController internal constructor(
|
||||
return
|
||||
}
|
||||
if (state == "error") {
|
||||
updateErrorText(payload["errorMessage"].asStringOrNull() ?: nativeString("Chat failed"))
|
||||
updateLocalizedErrorText(payload["errorMessage"].asStringOrNull()?.let(::verbatimText) ?: nativeText("Chat failed"))
|
||||
}
|
||||
if (runId != null) {
|
||||
clearPendingRun(runId)
|
||||
@@ -2601,7 +2618,7 @@ class ChatController internal constructor(
|
||||
}
|
||||
}
|
||||
"error" -> {
|
||||
updateErrorText(nativeString("Event stream interrupted; try refreshing."))
|
||||
updateLocalizedErrorText(nativeText("Event stream interrupted; try refreshing."))
|
||||
clearPendingRuns()
|
||||
pendingToolCallsById.clear()
|
||||
publishPendingToolCalls()
|
||||
@@ -2681,7 +2698,7 @@ class ChatController internal constructor(
|
||||
unresolvedRepliesByRunId.remove(runId)
|
||||
terminalWithoutReplyRunIds.remove(runId)
|
||||
timedOutRunIds.add(runId)
|
||||
updateErrorText(nativeString("Timed out waiting for a reply; try again or refresh."))
|
||||
updateLocalizedErrorText(nativeText("Timed out waiting for a reply; try again or refresh."))
|
||||
// The optimistic bubble is gone, so the journaled row must stay visible for review;
|
||||
// history proof still retires it later if the turn did persist.
|
||||
parkUnconfirmedDurableSend(runId)
|
||||
@@ -2873,7 +2890,7 @@ class ChatController internal constructor(
|
||||
unresolvedRunIds.forEach(::removeOptimisticMessage)
|
||||
unresolvedRunIds.forEach(unresolvedRepliesByRunId::remove)
|
||||
unresolvedRunIds.forEach(terminalWithoutReplyRunIds::remove)
|
||||
updateErrorText(nativeString("Timed out confirming the sent message; refresh to check delivery."))
|
||||
updateLocalizedErrorText(nativeText("Timed out confirming the sent message; refresh to check delivery."))
|
||||
// Ownership expired without proof; keep the journaled copies visible for manual review.
|
||||
for (unresolvedRunId in unresolvedRunIds) {
|
||||
parkUnconfirmedDurableSend(unresolvedRunId)
|
||||
|
||||
@@ -546,7 +546,12 @@ private fun TalkTranscript(
|
||||
items(entries.takeLast(6), key = { it.id }) { entry ->
|
||||
TalkTranscriptCard(
|
||||
label = if (entry.role == VoiceConversationRole.User) nativeString("You") else nativeString("OpenClaw"),
|
||||
text = if (entry.isStreaming && entry.text.isBlank()) nativeString("Listening response...") else entry.text,
|
||||
text =
|
||||
if (entry.isStreaming && entry.text.isBlank()) {
|
||||
nativeString("Listening response...")
|
||||
} else {
|
||||
entry.localizedSource?.let(::nativeString) ?: entry.text
|
||||
},
|
||||
muted = entry.isStreaming,
|
||||
)
|
||||
}
|
||||
@@ -1047,7 +1052,7 @@ private fun VoiceTurnCard(entry: VoiceConversationEntry) {
|
||||
if (entry.isStreaming && entry.text.isBlank()) {
|
||||
nativeString("Listening...")
|
||||
} else {
|
||||
entry.text
|
||||
entry.localizedSource?.let(::nativeString) ?: entry.text
|
||||
},
|
||||
style = ClawTheme.type.body,
|
||||
color = ClawTheme.colors.text,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package ai.openclaw.app.voice
|
||||
|
||||
import ai.openclaw.app.gateway.ChatSendAck
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.i18n.NativeText
|
||||
import ai.openclaw.app.i18n.nativeText
|
||||
import ai.openclaw.app.i18n.resolveNativeText
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -39,6 +41,7 @@ data class VoiceConversationEntry(
|
||||
val role: VoiceConversationRole,
|
||||
val text: String,
|
||||
val isStreaming: Boolean = false,
|
||||
val localizedSource: String? = null,
|
||||
)
|
||||
|
||||
internal data class GatewayTranscriptionSession(
|
||||
@@ -88,8 +91,8 @@ internal class MicCaptureManager(
|
||||
private val _isListening = MutableStateFlow(false)
|
||||
val isListening: StateFlow<Boolean> = _isListening
|
||||
|
||||
private val _statusText = MutableStateFlow("Mic off")
|
||||
val statusText: StateFlow<String> = _statusText
|
||||
private val _statusText = MutableStateFlow<NativeText>(nativeText("Mic off"))
|
||||
val statusText: StateFlow<String> = _statusText.resolveNativeText()
|
||||
|
||||
private val _liveTranscript = MutableStateFlow<String?>(null)
|
||||
val liveTranscript: StateFlow<String?> = _liveTranscript
|
||||
@@ -175,7 +178,7 @@ internal class MicCaptureManager(
|
||||
}
|
||||
}
|
||||
if (pausedForTts) {
|
||||
_statusText.value = if (_isSending.value) nativeString("Speaking · waiting for reply") else nativeString("Speaking…")
|
||||
_statusText.value = if (_isSending.value) nativeText("Speaking · waiting for reply") else nativeText("Speaking…")
|
||||
return
|
||||
}
|
||||
transcriptionDrainJob?.cancel()
|
||||
@@ -226,7 +229,7 @@ internal class MicCaptureManager(
|
||||
_isListening.value = false
|
||||
_inputLevel.value = 0f
|
||||
_liveTranscript.value = null
|
||||
_statusText.value = if (_isSending.value) nativeString("Speaking · waiting for reply") else nativeString("Speaking…")
|
||||
_statusText.value = if (_isSending.value) nativeText("Speaking · waiting for reply") else nativeText("Speaking…")
|
||||
true
|
||||
}
|
||||
if (!shouldPause) return
|
||||
@@ -245,10 +248,10 @@ internal class MicCaptureManager(
|
||||
if (!resume) {
|
||||
_statusText.value =
|
||||
when {
|
||||
_micEnabled.value && _isSending.value -> nativeString("Listening · sending queued voice")
|
||||
_micEnabled.value -> nativeString("Listening")
|
||||
_isSending.value -> nativeString("Mic off · sending…")
|
||||
else -> nativeString("Mic off")
|
||||
_micEnabled.value && _isSending.value -> nativeText("Listening · sending queued voice")
|
||||
_micEnabled.value -> nativeText("Listening")
|
||||
_isSending.value -> nativeText("Mic off · sending…")
|
||||
else -> nativeText("Mic off")
|
||||
}
|
||||
}
|
||||
resume
|
||||
@@ -305,7 +308,7 @@ internal class MicCaptureManager(
|
||||
_isSending.value = false
|
||||
stopRequested = true
|
||||
stopTranscription(preserveStatus = true)
|
||||
_statusText.value = if (_micEnabled.value) nativeString("Mic on · waiting for gateway") else nativeString("Mic off")
|
||||
_statusText.value = if (_micEnabled.value) nativeText("Mic on · waiting for gateway") else nativeText("Mic off")
|
||||
}
|
||||
|
||||
internal fun submitTranscribedMessage(text: String) {
|
||||
@@ -370,7 +373,12 @@ internal class MicCaptureManager(
|
||||
completePendingTurn()
|
||||
}
|
||||
"aborted" -> {
|
||||
upsertPendingAssistant(text = nativeString("Response aborted"), isStreaming = false)
|
||||
val abortedText = nativeText("Response aborted")
|
||||
upsertPendingAssistant(
|
||||
text = abortedText.resolveNativeText(),
|
||||
isStreaming = false,
|
||||
localizedSource = abortedText.source,
|
||||
)
|
||||
completePendingTurn()
|
||||
}
|
||||
}
|
||||
@@ -379,12 +387,12 @@ internal class MicCaptureManager(
|
||||
private fun start() {
|
||||
stopRequested = false
|
||||
if (!hasMicPermission()) {
|
||||
_statusText.value = nativeString("Microphone permission required")
|
||||
_statusText.value = nativeText("Microphone permission required")
|
||||
_micEnabled.value = false
|
||||
return
|
||||
}
|
||||
if (!gatewayConnected) {
|
||||
_statusText.value = nativeString("Mic on · waiting for gateway")
|
||||
_statusText.value = nativeText("Mic on · waiting for gateway")
|
||||
return
|
||||
}
|
||||
if (transcriptionSession != null || transcriptionStartJob?.isActive == true) return
|
||||
@@ -409,7 +417,7 @@ internal class MicCaptureManager(
|
||||
return@launch
|
||||
}
|
||||
val message = err.message ?: err::class.simpleName.orEmpty()
|
||||
_statusText.value = nativeString("Transcription unavailable: \$message", message)
|
||||
_statusText.value = nativeText("Transcription unavailable: \$message", message)
|
||||
_micEnabled.value = false
|
||||
stopTranscription(preserveStatus = true)
|
||||
} finally {
|
||||
@@ -448,7 +456,7 @@ internal class MicCaptureManager(
|
||||
_isListening.value = false
|
||||
_inputLevel.value = 0f
|
||||
if (!preserveStatus) {
|
||||
_statusText.value = if (_isSending.value) nativeString("Mic off · sending…") else nativeString("Mic off")
|
||||
_statusText.value = if (_isSending.value) nativeText("Mic off · sending…") else nativeText("Mic off")
|
||||
} else {
|
||||
_statusText.value = status
|
||||
}
|
||||
@@ -499,9 +507,9 @@ internal class MicCaptureManager(
|
||||
if (_isSending.value) return
|
||||
if (!hasQueuedMessages()) {
|
||||
if (_micEnabled.value) {
|
||||
_statusText.value = nativeString("Listening")
|
||||
_statusText.value = nativeText("Listening")
|
||||
} else {
|
||||
_statusText.value = nativeString("Mic off")
|
||||
_statusText.value = nativeText("Mic off")
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -514,7 +522,7 @@ internal class MicCaptureManager(
|
||||
_isSending.value = true
|
||||
pendingRunTimeoutJob?.cancel()
|
||||
pendingRunTimeoutJob = null
|
||||
_statusText.value = if (_micEnabled.value) nativeString("Listening · sending queued voice") else nativeString("Sending queued voice")
|
||||
_statusText.value = if (_micEnabled.value) nativeText("Listening · sending queued voice") else nativeText("Sending queued voice")
|
||||
|
||||
val sendGeneration = gatewayGeneration
|
||||
sendJob =
|
||||
@@ -539,7 +547,7 @@ internal class MicCaptureManager(
|
||||
}
|
||||
ack.isTerminalFailure -> {
|
||||
completePendingTurn()
|
||||
_statusText.value = nativeString("Send failed: Chat failed before the run started; try again.")
|
||||
_statusText.value = nativeText("Send failed: Chat failed before the run started; try again.")
|
||||
}
|
||||
runId == null -> {
|
||||
completePendingTurn()
|
||||
@@ -561,7 +569,7 @@ internal class MicCaptureManager(
|
||||
if (!gatewayConnected) {
|
||||
queuedWaitingStatus()
|
||||
} else {
|
||||
"Send failed: ${err.message ?: err::class.simpleName}"
|
||||
nativeText("Send failed: \$message", err.message ?: err::class.simpleName.orEmpty())
|
||||
}
|
||||
} finally {
|
||||
if (sendGeneration == gatewayGeneration) {
|
||||
@@ -582,7 +590,7 @@ internal class MicCaptureManager(
|
||||
_isSending.value = false
|
||||
_statusText.value =
|
||||
if (gatewayConnected) {
|
||||
"Voice reply timed out; retrying queued turn"
|
||||
nativeText("Voice reply timed out; retrying queued turn")
|
||||
} else {
|
||||
queuedWaitingStatus()
|
||||
}
|
||||
@@ -602,17 +610,26 @@ internal class MicCaptureManager(
|
||||
sendQueuedIfIdle()
|
||||
}
|
||||
|
||||
private fun queuedWaitingStatus(): String = nativeString("\${queuedMessageCount()} queued · waiting for gateway", queuedMessageCount())
|
||||
private fun queuedWaitingStatus(): NativeText = nativeText("\${queuedMessageCount()} queued · waiting for gateway", queuedMessageCount())
|
||||
|
||||
private fun appendConversation(
|
||||
role: VoiceConversationRole,
|
||||
text: String,
|
||||
isStreaming: Boolean = false,
|
||||
localizedSource: String? = null,
|
||||
): String {
|
||||
val id = UUID.randomUUID().toString()
|
||||
_conversation.value =
|
||||
(_conversation.value + VoiceConversationEntry(id = id, role = role, text = text, isStreaming = isStreaming))
|
||||
.takeLast(maxConversationEntries)
|
||||
(
|
||||
_conversation.value +
|
||||
VoiceConversationEntry(
|
||||
id = id,
|
||||
role = role,
|
||||
text = text,
|
||||
isStreaming = isStreaming,
|
||||
localizedSource = localizedSource,
|
||||
)
|
||||
).takeLast(maxConversationEntries)
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -620,6 +637,7 @@ internal class MicCaptureManager(
|
||||
id: String,
|
||||
text: String?,
|
||||
isStreaming: Boolean,
|
||||
localizedSource: String? = null,
|
||||
) {
|
||||
val current = _conversation.value
|
||||
if (current.isEmpty()) return
|
||||
@@ -633,15 +651,16 @@ internal class MicCaptureManager(
|
||||
|
||||
val entry = current[targetIndex]
|
||||
val updatedText = text ?: entry.text
|
||||
if (updatedText == entry.text && entry.isStreaming == isStreaming) return
|
||||
if (updatedText == entry.text && entry.isStreaming == isStreaming && entry.localizedSource == localizedSource) return
|
||||
val updated = current.toMutableList()
|
||||
updated[targetIndex] = entry.copy(text = updatedText, isStreaming = isStreaming)
|
||||
updated[targetIndex] = entry.copy(text = updatedText, isStreaming = isStreaming, localizedSource = localizedSource)
|
||||
_conversation.value = updated
|
||||
}
|
||||
|
||||
private fun upsertPendingAssistant(
|
||||
text: String,
|
||||
isStreaming: Boolean,
|
||||
localizedSource: String? = null,
|
||||
) {
|
||||
val currentId = pendingAssistantEntryId
|
||||
if (currentId == null) {
|
||||
@@ -650,10 +669,16 @@ internal class MicCaptureManager(
|
||||
role = VoiceConversationRole.Assistant,
|
||||
text = text,
|
||||
isStreaming = isStreaming,
|
||||
localizedSource = localizedSource,
|
||||
)
|
||||
return
|
||||
}
|
||||
updateConversationEntry(id = currentId, text = text, isStreaming = isStreaming)
|
||||
updateConversationEntry(
|
||||
id = currentId,
|
||||
text = text,
|
||||
isStreaming = isStreaming,
|
||||
localizedSource = localizedSource,
|
||||
)
|
||||
}
|
||||
|
||||
private fun playAssistantReplyAsync(text: String) {
|
||||
@@ -775,16 +800,16 @@ internal class MicCaptureManager(
|
||||
message: String,
|
||||
) {
|
||||
if (transcriptionSession != session) return
|
||||
_statusText.value = nativeString("Transcription failed: \$message", message)
|
||||
_statusText.value = nativeText("Transcription failed: \$message", message)
|
||||
_micEnabled.value = false
|
||||
stopTranscription(preserveStatus = true)
|
||||
}
|
||||
|
||||
private fun listeningStatus(): String =
|
||||
private fun listeningStatus(): NativeText =
|
||||
when {
|
||||
_isSending.value -> nativeString("Listening · sending queued voice")
|
||||
hasQueuedMessages() -> nativeString("Listening · \${queuedMessageCount()} queued", queuedMessageCount())
|
||||
else -> nativeString("Listening")
|
||||
_isSending.value -> nativeText("Listening · sending queued voice")
|
||||
hasQueuedMessages() -> nativeText("Listening · \${queuedMessageCount()} queued", queuedMessageCount())
|
||||
else -> nativeText("Listening")
|
||||
}
|
||||
|
||||
private fun pcm16ToPcmu(pcm16: ByteArray): ByteArray {
|
||||
|
||||
@@ -4,7 +4,9 @@ import ai.openclaw.app.gateway.ChatSendAck
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.gateway.chatSendAckHistorySinceSeconds
|
||||
import ai.openclaw.app.gateway.parseChatSendAck
|
||||
import ai.openclaw.app.i18n.nativeString
|
||||
import ai.openclaw.app.i18n.NativeText
|
||||
import ai.openclaw.app.i18n.nativeText
|
||||
import ai.openclaw.app.i18n.resolveNativeText
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
@@ -196,8 +198,8 @@ class TalkModeManager internal constructor(
|
||||
private val _speechActive = MutableStateFlow(false)
|
||||
val speechActive: StateFlow<Boolean> = _speechActive
|
||||
|
||||
private val _statusText = MutableStateFlow("Off")
|
||||
val statusText: StateFlow<String> = _statusText
|
||||
private val _statusText = MutableStateFlow<NativeText>(nativeText("Off"))
|
||||
val statusText: StateFlow<String> = _statusText.resolveNativeText()
|
||||
|
||||
// Typed "waiting on the agent" signal for the waveform's Thinking phase, so
|
||||
// UI never has to parse status strings. Every status change flows through
|
||||
@@ -206,7 +208,7 @@ class TalkModeManager internal constructor(
|
||||
val awaitingAgent: StateFlow<Boolean> = _awaitingAgent
|
||||
|
||||
private fun setStatus(
|
||||
text: String,
|
||||
text: NativeText,
|
||||
awaitingAgent: Boolean = false,
|
||||
) {
|
||||
_statusText.value = text
|
||||
@@ -451,7 +453,7 @@ class TalkModeManager internal constructor(
|
||||
throw IllegalStateException("PTT_BUSY: previous push-to-talk turn is still finishing")
|
||||
}
|
||||
if (!isConnected()) {
|
||||
setStatus("Gateway not connected")
|
||||
setStatus(nativeText("Gateway not connected"))
|
||||
throw IllegalStateException("UNAVAILABLE: Gateway not connected")
|
||||
}
|
||||
|
||||
@@ -459,11 +461,11 @@ class TalkModeManager internal constructor(
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (!micOk) {
|
||||
setStatus("Microphone permission required")
|
||||
setStatus(nativeText("Microphone permission required"))
|
||||
throw IllegalStateException("MIC_PERMISSION_REQUIRED: grant Microphone permission")
|
||||
}
|
||||
if (!SpeechRecognizer.isRecognitionAvailable(context)) {
|
||||
setStatus("Speech recognizer unavailable")
|
||||
setStatus(nativeText("Speech recognizer unavailable"))
|
||||
throw IllegalStateException("UNAVAILABLE: Speech recognizer unavailable")
|
||||
}
|
||||
|
||||
@@ -524,10 +526,10 @@ class TalkModeManager internal constructor(
|
||||
pttCompletion = null
|
||||
completion?.cancel()
|
||||
resumeRealtimeCaptureAfterPushToTalk(captureId)
|
||||
setStatus(if (_isEnabled.value) nativeString("Listening") else nativeString("Ready"))
|
||||
setStatus(if (_isEnabled.value) nativeText("Listening") else nativeText("Ready"))
|
||||
throw err
|
||||
}
|
||||
setStatus("Listening (PTT)")
|
||||
setStatus(nativeText("Listening (PTT)"))
|
||||
if (autoStopAfterMs != null) {
|
||||
pttAutoStopEnabled = true
|
||||
// Install one-shot jobs before yielding to lifecycle changes. Otherwise a
|
||||
@@ -565,7 +567,7 @@ class TalkModeManager internal constructor(
|
||||
val transcript = cleared.transcript
|
||||
|
||||
if (transcript.isEmpty()) {
|
||||
setStatus(if (_isEnabled.value) nativeString("Listening") else nativeString("Ready"))
|
||||
setStatus(if (_isEnabled.value) nativeText("Listening") else nativeText("Ready"))
|
||||
resumeRealtimeCaptureAfterPushToTalk(captureId)
|
||||
return@withContext finishPushToTalk(
|
||||
TalkPttStopPayload(captureId = captureId, transcript = null, status = "empty"),
|
||||
@@ -574,7 +576,7 @@ class TalkModeManager internal constructor(
|
||||
}
|
||||
|
||||
if (!isConnected()) {
|
||||
setStatus("Gateway not connected")
|
||||
setStatus(nativeText("Gateway not connected"))
|
||||
resumeRealtimeCaptureAfterPushToTalk(captureId)
|
||||
return@withContext finishPushToTalk(
|
||||
TalkPttStopPayload(captureId = captureId, transcript = transcript, status = "offline"),
|
||||
@@ -582,7 +584,7 @@ class TalkModeManager internal constructor(
|
||||
)
|
||||
}
|
||||
|
||||
setStatus("Thinking…", awaitingAgent = true)
|
||||
setStatus(nativeText("Thinking…"), awaitingAgent = true)
|
||||
lateinit var finishingJob: Job
|
||||
finishingJob =
|
||||
// Gateway-scoped so a switch drops the stale finalize; the NonCancellable
|
||||
@@ -624,7 +626,7 @@ class TalkModeManager internal constructor(
|
||||
val cleared =
|
||||
clearPushToTalkRecognition(captureId)
|
||||
?: return@withContext TalkPttStopPayload(captureId = captureId, transcript = null, status = "idle")
|
||||
setStatus(if (_isEnabled.value) nativeString("Listening") else nativeString("Ready"))
|
||||
setStatus(if (_isEnabled.value) nativeText("Listening") else nativeText("Ready"))
|
||||
resumeRealtimeCaptureAfterPushToTalk(captureId)
|
||||
finishPushToTalk(
|
||||
TalkPttStopPayload(captureId = captureId, transcript = null, status = "cancelled"),
|
||||
@@ -833,7 +835,7 @@ class TalkModeManager internal constructor(
|
||||
startRealtimeRelay(generation)
|
||||
} catch (err: Throwable) {
|
||||
if (err is CancellationException) return@launch
|
||||
setStatus("Start failed: ${err.message ?: err::class.simpleName}")
|
||||
setStatus(nativeText("Start failed: \$message", err.message ?: err::class.simpleName.orEmpty()))
|
||||
Log.w(tag, "start failed: ${err.message ?: err::class.simpleName}")
|
||||
stopRealtimeRelay(closeSession = false, preserveStatus = true)
|
||||
disableRealtimeModeAndNotifyOwner()
|
||||
@@ -862,7 +864,7 @@ class TalkModeManager internal constructor(
|
||||
lastTranscript = ""
|
||||
lastHeardAtMs = null
|
||||
_isListening.value = false
|
||||
setStatus("Off")
|
||||
setStatus(nativeText("Off"))
|
||||
stopRealtimeRelay()
|
||||
stopSpeaking()
|
||||
pendingRunId = null
|
||||
@@ -885,7 +887,7 @@ class TalkModeManager internal constructor(
|
||||
withTimeout(timeoutMs) {
|
||||
while (true) {
|
||||
realtimeSessionId?.let { return@withTimeout it }
|
||||
val status = _statusText.value
|
||||
val status = _statusText.value.resolveNativeText()
|
||||
if (!_isEnabled.value && status != "Off") {
|
||||
throw IllegalStateException(status)
|
||||
}
|
||||
@@ -896,7 +898,7 @@ class TalkModeManager internal constructor(
|
||||
|
||||
private suspend fun startRealtimeRelay(generation: Long) {
|
||||
if (!isConnected()) {
|
||||
setStatus("Gateway not connected")
|
||||
setStatus(nativeText("Gateway not connected"))
|
||||
Log.w(tag, "realtime start: gateway not connected")
|
||||
disableRealtimeModeAndNotifyOwner()
|
||||
return
|
||||
@@ -906,7 +908,7 @@ class TalkModeManager internal constructor(
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
if (!micOk) {
|
||||
setStatus("Microphone permission required")
|
||||
setStatus(nativeText("Microphone permission required"))
|
||||
Log.w(tag, "realtime start: microphone permission required")
|
||||
disableRealtimeModeAndNotifyOwner()
|
||||
return
|
||||
@@ -923,7 +925,7 @@ class TalkModeManager internal constructor(
|
||||
}
|
||||
}
|
||||
|
||||
setStatus("Connecting…", awaitingAgent = true)
|
||||
setStatus(nativeText("Connecting…"), awaitingAgent = true)
|
||||
val params =
|
||||
buildJsonObject {
|
||||
put("sessionKey", JsonPrimitive(mainSessionKey.ifBlank { "main" }))
|
||||
@@ -956,7 +958,7 @@ class TalkModeManager internal constructor(
|
||||
} else {
|
||||
realtimeOutputSuppressed = false
|
||||
_isListening.value = true
|
||||
setStatus("Listening")
|
||||
setStatus(nativeText("Listening"))
|
||||
startRealtimeCaptureLocked(sessionId)
|
||||
false
|
||||
}
|
||||
@@ -980,16 +982,16 @@ class TalkModeManager internal constructor(
|
||||
message: String,
|
||||
) {
|
||||
if (realtimeSessionId != sessionId) return
|
||||
setStatus("Talk failed: $message")
|
||||
setStatus(nativeText("Talk failed: \$message", message))
|
||||
stopRealtimeRelay(cancelCapture = false, cancelAppend = false, preserveStatus = true)
|
||||
disableRealtimeModeAndNotifyOwner()
|
||||
}
|
||||
|
||||
private fun realtimeCloseStatusText(reason: String?): String =
|
||||
private fun realtimeCloseStatusText(reason: String?): NativeText =
|
||||
when (reason) {
|
||||
null, "completed" -> nativeString("Off")
|
||||
"error" -> nativeString("Talk failed: Realtime provider closed unexpectedly.")
|
||||
else -> nativeString("Talk failed: Realtime provider closed: \$reason", reason)
|
||||
null, "completed" -> nativeText("Off")
|
||||
"error" -> nativeText("Talk failed: Realtime provider closed unexpectedly.")
|
||||
else -> nativeText("Talk failed: Realtime provider closed: \$reason", reason)
|
||||
}
|
||||
|
||||
/** Caller holds [realtimeCapturePauseLock] so PTT cannot miss newly installed jobs. */
|
||||
@@ -1077,7 +1079,7 @@ class TalkModeManager internal constructor(
|
||||
"ready" -> {
|
||||
if (isRealtimeCapturePaused()) return
|
||||
_isListening.value = true
|
||||
setStatus("Listening")
|
||||
setStatus(nativeText("Listening"))
|
||||
}
|
||||
"inputAudio" -> {
|
||||
synchronized(realtimeCapturePauseLock) {
|
||||
@@ -1129,7 +1131,7 @@ class TalkModeManager internal constructor(
|
||||
_lastAssistantText.value = assistantText.trim()
|
||||
}
|
||||
if (isFinal && role == "user") {
|
||||
setStatus("Thinking…", awaitingAgent = true)
|
||||
setStatus(nativeText("Thinking…"), awaitingAgent = true)
|
||||
} else if (isFinal && role == "assistant") {
|
||||
scheduleRealtimePlaybackIdle()
|
||||
}
|
||||
@@ -1147,14 +1149,14 @@ class TalkModeManager internal constructor(
|
||||
"toolResult" -> Unit
|
||||
"error" -> {
|
||||
val message = obj["message"].asStringOrNull() ?: "realtime talk error"
|
||||
setStatus("Talk failed: $message")
|
||||
setStatus(nativeText("Talk failed: \$message", message))
|
||||
Log.w(tag, "realtime error: $message")
|
||||
}
|
||||
"close" -> {
|
||||
val closeReason = obj["reason"].asStringOrNull()?.trim()?.takeIf(String::isNotEmpty)
|
||||
val currentStatus = _statusText.value
|
||||
val closeStatus =
|
||||
if (currentStatus.startsWith("Talk failed:")) currentStatus else realtimeCloseStatusText(closeReason)
|
||||
if (currentStatus.resolveNativeText().startsWith("Talk failed:")) currentStatus else realtimeCloseStatusText(closeReason)
|
||||
Log.d(tag, "realtime close reason=$closeReason")
|
||||
stopRealtimeRelay(closeSession = false, preserveStatus = true)
|
||||
if (_isEnabled.value) {
|
||||
@@ -1267,7 +1269,7 @@ class TalkModeManager internal constructor(
|
||||
_outputLevel.value =
|
||||
TalkAudioLevel.smoothed(_outputLevel.value ?: 0f, TalkAudioLevel.pcm16Level(bytes, writtenBytes))
|
||||
_isSpeaking.value = true
|
||||
setStatus("Speaking…")
|
||||
setStatus(nativeText("Speaking…"))
|
||||
val durationMs = ((writtenBytes / 2.0) / realtimeSampleRateHz * 1000.0).toLong()
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
realtimePlaybackEndsAtMs = maxOf(now, realtimePlaybackEndsAtMs) + durationMs
|
||||
@@ -1291,7 +1293,7 @@ class TalkModeManager internal constructor(
|
||||
playbackIdle
|
||||
}
|
||||
if (idle && _isEnabled.value && realtimeSessionId != null) {
|
||||
setStatus("Listening")
|
||||
setStatus(nativeText("Listening"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1321,7 +1323,7 @@ class TalkModeManager internal constructor(
|
||||
_isSpeaking.value = false
|
||||
_outputLevel.value = null
|
||||
if (_isEnabled.value) {
|
||||
setStatus("Listening")
|
||||
setStatus(nativeText("Listening"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1438,7 +1440,7 @@ class TalkModeManager internal constructor(
|
||||
}
|
||||
realtimeCapturePause = null
|
||||
_isListening.value = true
|
||||
setStatus("Listening")
|
||||
setStatus(nativeText("Listening"))
|
||||
startRealtimeCaptureLocked(sessionId)
|
||||
RealtimeCaptureResume.Resumed
|
||||
}
|
||||
@@ -1447,7 +1449,7 @@ class TalkModeManager internal constructor(
|
||||
RealtimeCaptureResume.Resumed -> return
|
||||
RealtimeCaptureResume.Restart -> start()
|
||||
RealtimeCaptureResume.Disconnected -> {
|
||||
setStatus("Gateway not connected")
|
||||
setStatus(nativeText("Gateway not connected"))
|
||||
stopRealtimeRelay(preserveStatus = true)
|
||||
disableRealtimeModeAndNotifyOwner()
|
||||
}
|
||||
@@ -1498,7 +1500,7 @@ class TalkModeManager internal constructor(
|
||||
if (!runId.isNullOrBlank()) {
|
||||
when (val registration = registerRealtimeToolRun(runId, callId, relaySessionId)) {
|
||||
RealtimeToolRegistration.SessionEnded -> return@launch
|
||||
RealtimeToolRegistration.AwaitingCompletion -> setStatus("Thinking…", awaitingAgent = true)
|
||||
RealtimeToolRegistration.AwaitingCompletion -> setStatus(nativeText("Thinking…"), awaitingAgent = true)
|
||||
is RealtimeToolRegistration.Completed ->
|
||||
dispatchRealtimeToolCompletion(registration.dispatch)
|
||||
}
|
||||
@@ -1929,7 +1931,7 @@ class TalkModeManager internal constructor(
|
||||
}
|
||||
|
||||
if (markListening) {
|
||||
setStatus("Listening")
|
||||
setStatus(nativeText("Listening"))
|
||||
_isListening.value = true
|
||||
}
|
||||
r.startListening(intent)
|
||||
@@ -2021,7 +2023,7 @@ class TalkModeManager internal constructor(
|
||||
private suspend fun finalizeTranscript(transcript: String) {
|
||||
listeningMode = false
|
||||
_isListening.value = false
|
||||
setStatus("Thinking…", awaitingAgent = true)
|
||||
setStatus(nativeText("Thinking…"), awaitingAgent = true)
|
||||
lastTranscript = ""
|
||||
lastHeardAtMs = null
|
||||
// Release SpeechRecognizer before making the API call and playing TTS.
|
||||
@@ -2038,7 +2040,7 @@ class TalkModeManager internal constructor(
|
||||
ensureConfigLoaded()
|
||||
val prompt = buildPrompt(transcript)
|
||||
if (!isConnected()) {
|
||||
setStatus("Gateway not connected")
|
||||
setStatus(nativeText("Gateway not connected"))
|
||||
Log.w(tag, "finalize: gateway not connected")
|
||||
start()
|
||||
return
|
||||
@@ -2051,7 +2053,7 @@ class TalkModeManager internal constructor(
|
||||
val runId = ack.runId ?: throw IllegalStateException("chat.send returned no run id")
|
||||
Log.d(tag, "chat.send ok runId=$runId status=${ack.status}")
|
||||
if (ack.isTerminalFailure) {
|
||||
setStatus(if (ack.normalizedStatus == "error") nativeString("Chat error") else nativeString("Aborted"))
|
||||
setStatus(if (ack.normalizedStatus == "error") nativeText("Chat error") else nativeText("Aborted"))
|
||||
start()
|
||||
return
|
||||
}
|
||||
@@ -2068,7 +2070,7 @@ class TalkModeManager internal constructor(
|
||||
if (ok) 12_000 else 25_000,
|
||||
)
|
||||
if (assistant.isNullOrBlank()) {
|
||||
setStatus("No reply")
|
||||
setStatus(nativeText("No reply"))
|
||||
Log.w(tag, "assistant text timeout runId=$runId")
|
||||
start()
|
||||
return
|
||||
@@ -2084,7 +2086,7 @@ class TalkModeManager internal constructor(
|
||||
Log.d(tag, "finalize speech cancelled")
|
||||
return
|
||||
}
|
||||
setStatus("Talk failed: ${err.message ?: err::class.simpleName}")
|
||||
setStatus(nativeText("Talk failed: \$message", err.message ?: err::class.simpleName.orEmpty()))
|
||||
Log.w(tag, "finalize failed: ${err.message ?: err::class.simpleName}")
|
||||
}
|
||||
|
||||
@@ -2309,7 +2311,7 @@ class TalkModeManager internal constructor(
|
||||
_lastAssistantText.value = cleaned
|
||||
ensurePlaybackActive(playbackToken)
|
||||
|
||||
setStatus("Generating voice…", awaitingAgent = true)
|
||||
setStatus(nativeText("Generating voice…"), awaitingAgent = true)
|
||||
_isSpeaking.value = false
|
||||
lastSpokenText = cleaned
|
||||
|
||||
@@ -2337,7 +2339,7 @@ class TalkModeManager internal constructor(
|
||||
Log.d(tag, "assistant speech cancelled")
|
||||
return
|
||||
}
|
||||
setStatus("Speak failed: ${err.message ?: err::class.simpleName}")
|
||||
setStatus(nativeText("Speak failed: \$message", err.message ?: err::class.simpleName.orEmpty()))
|
||||
Log.w(tag, "talk playback failed: ${err.message ?: err::class.simpleName}")
|
||||
} finally {
|
||||
_isSpeaking.value = false
|
||||
@@ -2485,7 +2487,7 @@ class TalkModeManager internal constructor(
|
||||
|
||||
private fun markAudioPlaybackStarting(playbackToken: Long) {
|
||||
ensurePlaybackActive(playbackToken)
|
||||
setStatus("Speaking…")
|
||||
setStatus(nativeText("Speaking…"))
|
||||
_isSpeaking.value = true
|
||||
ensureInterruptListener()
|
||||
requestAudioFocusForTts()
|
||||
@@ -2497,7 +2499,7 @@ class TalkModeManager internal constructor(
|
||||
scope.launch { cancelRealtimeOutput(reason = "android-stop-tts") }
|
||||
stopSpeaking(resetInterrupt = true)
|
||||
_isSpeaking.value = false
|
||||
setStatus("Listening")
|
||||
setStatus(nativeText("Listening"))
|
||||
}
|
||||
|
||||
private suspend fun cancelRealtimeOutput(reason: String): Boolean =
|
||||
@@ -2775,7 +2777,7 @@ class TalkModeManager internal constructor(
|
||||
// Only a live listening session may claim the status; a speech-interrupt
|
||||
// recognizer readying during playback must not touch Thinking state.
|
||||
if (_isEnabled.value && _isListening.value) {
|
||||
setStatus("Listening")
|
||||
setStatus(nativeText("Listening"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2798,21 +2800,21 @@ class TalkModeManager internal constructor(
|
||||
if (stopRequested) return
|
||||
_isListening.value = false
|
||||
if (error == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS) {
|
||||
setStatus(nativeString("Microphone permission required"))
|
||||
setStatus(nativeText("Microphone permission required"))
|
||||
return
|
||||
}
|
||||
|
||||
setStatus(
|
||||
when (error) {
|
||||
SpeechRecognizer.ERROR_AUDIO -> nativeString("Audio error")
|
||||
SpeechRecognizer.ERROR_CLIENT -> nativeString("Client error")
|
||||
SpeechRecognizer.ERROR_NETWORK -> nativeString("Network error")
|
||||
SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> nativeString("Network timeout")
|
||||
SpeechRecognizer.ERROR_NO_MATCH -> nativeString("Listening")
|
||||
SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> nativeString("Recognizer busy")
|
||||
SpeechRecognizer.ERROR_SERVER -> nativeString("Server error")
|
||||
SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> nativeString("Listening")
|
||||
else -> nativeString("Speech error (\$error)", error)
|
||||
SpeechRecognizer.ERROR_AUDIO -> nativeText("Audio error")
|
||||
SpeechRecognizer.ERROR_CLIENT -> nativeText("Client error")
|
||||
SpeechRecognizer.ERROR_NETWORK -> nativeText("Network error")
|
||||
SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> nativeText("Network timeout")
|
||||
SpeechRecognizer.ERROR_NO_MATCH -> nativeText("Listening")
|
||||
SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> nativeText("Recognizer busy")
|
||||
SpeechRecognizer.ERROR_SERVER -> nativeText("Server error")
|
||||
SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> nativeText("Listening")
|
||||
else -> nativeText("Speech error (\$error)", error)
|
||||
},
|
||||
)
|
||||
scheduleRestart(delayMs = 600)
|
||||
|
||||
@@ -4,6 +4,7 @@ import ai.openclaw.app.gateway.DeviceAuthEntry
|
||||
import ai.openclaw.app.gateway.DeviceAuthTokenStore
|
||||
import ai.openclaw.app.gateway.DeviceIdentityStore
|
||||
import ai.openclaw.app.gateway.GatewaySession
|
||||
import ai.openclaw.app.i18n.nativeText
|
||||
import android.Manifest
|
||||
import android.content.ComponentName
|
||||
import android.content.IntentFilter
|
||||
@@ -321,7 +322,7 @@ class TalkModeManagerTest {
|
||||
|
||||
setPrivateField(manager, "realtimeSessionId", "relay-1")
|
||||
setMutableStateFlow(manager, "_isEnabled", true)
|
||||
setMutableStateFlow(manager, "_statusText", "Talk failed: Provider rejected the session.")
|
||||
setMutableStateFlow(manager, "_statusText", nativeText("Talk failed: \$message", "Provider rejected the session."))
|
||||
|
||||
manager.handleGatewayEvent(
|
||||
"talk.event",
|
||||
@@ -773,7 +774,7 @@ class TalkModeManagerTest {
|
||||
setPrivateField(pause, "sessionId", "relay-1")
|
||||
setPrivateField(manager, "realtimeSessionId", "relay-1")
|
||||
setMutableStateFlow(manager, "_isListening", false)
|
||||
setMutableStateFlow(manager, "_statusText", "Thinking…")
|
||||
setMutableStateFlow(manager, "_statusText", nativeText("Thinking…"))
|
||||
|
||||
manager.resumeRealtimeCaptureAfterPushToTalk("capture-1")
|
||||
|
||||
@@ -829,7 +830,7 @@ class TalkModeManagerTest {
|
||||
val pause = readPrivateField(manager, "realtimeCapturePause")!!
|
||||
setPrivateField(pause, "restartRelay", true)
|
||||
setPrivateField(manager, "stopRequested", true)
|
||||
setMutableStateFlow(manager, "_statusText", "Off")
|
||||
setMutableStateFlow(manager, "_statusText", nativeText("Off"))
|
||||
|
||||
manager.resumeRealtimeCaptureAfterPushToTalk("capture-1")
|
||||
|
||||
@@ -901,7 +902,7 @@ class TalkModeManagerTest {
|
||||
setPrivateField(pause, "sessionId", "relay-1")
|
||||
setPrivateField(manager, "realtimeSessionId", "relay-1")
|
||||
setMutableStateFlow(manager, "_isListening", false)
|
||||
setMutableStateFlow(manager, "_statusText", "Gateway not connected")
|
||||
setMutableStateFlow(manager, "_statusText", nativeText("Gateway not connected"))
|
||||
|
||||
manager.resumeRealtimeCaptureAfterPushToTalk("capture-1")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user