diff --git a/apps/android/app/src/main/java/ai/openclaw/app/voice/MicCaptureManager.kt b/apps/android/app/src/main/java/ai/openclaw/app/voice/MicCaptureManager.kt index 6ef3ffade8e..dcb7300d0ca 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/voice/MicCaptureManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/voice/MicCaptureManager.kt @@ -90,6 +90,7 @@ class MicCaptureManager( val isSending: StateFlow = _isSending private val messageQueue = ArrayDeque() + private val messageQueueLock = Any() private var flushedPartialTranscript: String? = null private var pendingRunId: String? = null private var pendingAssistantEntryId: String? = null @@ -105,6 +106,42 @@ class MicCaptureManager( private var ttsPauseDepth = 0 private var resumeMicAfterTts = false + private fun enqueueMessage(message: String) { + synchronized(messageQueueLock) { + messageQueue.addLast(message) + } + } + + private fun snapshotMessageQueue(): List { + return synchronized(messageQueueLock) { + messageQueue.toList() + } + } + + private fun hasQueuedMessages(): Boolean { + return synchronized(messageQueueLock) { + messageQueue.isNotEmpty() + } + } + + private fun firstQueuedMessage(): String? { + return synchronized(messageQueueLock) { + messageQueue.firstOrNull() + } + } + + private fun removeFirstQueuedMessage(): String? { + return synchronized(messageQueueLock) { + if (messageQueue.isEmpty()) null else messageQueue.removeFirst() + } + } + + private fun queuedMessageCount(): Int { + return synchronized(messageQueueLock) { + messageQueue.size + } + } + fun setMicEnabled(enabled: Boolean) { if (_micEnabled.value == enabled) return _micEnabled.value = enabled @@ -348,7 +385,7 @@ class MicCaptureManager( role = VoiceConversationRole.User, text = message, ) - messageQueue.addLast(message) + enqueueMessage(message) publishQueue() } @@ -367,12 +404,12 @@ class MicCaptureManager( } private fun publishQueue() { - _queuedMessages.value = messageQueue.toList() + _queuedMessages.value = snapshotMessageQueue() } private fun sendQueuedIfIdle() { if (_isSending.value) return - if (messageQueue.isEmpty()) { + if (!hasQueuedMessages()) { if (_micEnabled.value) { _statusText.value = "Listening" } else { @@ -385,7 +422,7 @@ class MicCaptureManager( return } - val next = messageQueue.first() + val next = firstQueuedMessage() ?: return _isSending.value = true pendingRunTimeoutJob?.cancel() pendingRunTimeoutJob = null @@ -403,7 +440,7 @@ class MicCaptureManager( if (runId == null) { pendingRunTimeoutJob?.cancel() pendingRunTimeoutJob = null - messageQueue.removeFirst() + removeFirstQueuedMessage() publishQueue() _isSending.value = false pendingAssistantEntryId = null @@ -449,8 +486,7 @@ class MicCaptureManager( private fun completePendingTurn() { pendingRunTimeoutJob?.cancel() pendingRunTimeoutJob = null - if (messageQueue.isNotEmpty()) { - messageQueue.removeFirst() + if (removeFirstQueuedMessage() != null) { publishQueue() } pendingRunId = null @@ -460,7 +496,7 @@ class MicCaptureManager( } private fun queuedWaitingStatus(): String { - return "${messageQueue.size} queued · waiting for gateway" + return "${queuedMessageCount()} queued · waiting for gateway" } private fun appendConversation(