From dbec6e75e8452714d81d33b4c1b1137fac93145a Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 1 Oct 2026 18:13:35 +0300 Subject: [PATCH] Pin Android recording targets and stop capture outside the main thread --- .../utun/chat/ui/components/MessageBubble.kt | 7 +- .../com/utun/chat/viewmodel/ChatViewModel.kt | 72 ++++++++++--------- 2 files changed, 38 insertions(+), 41 deletions(-) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt index 4de1b355..6ed4b25a 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt @@ -296,12 +296,7 @@ private fun FileBubble(message: Message, textColor: Color, onDownload: (Message) private val VIDEO_EXTENSIONS = setOf("mp4", "m4v", "mov", "webm", "3gp", "mkv", "avi", "ts", "mpg", "mpeg") private fun isVideoMessage(message: Message): Boolean { - if (message.contentType.startsWith("video/")) return true - if (message.contentType == "application/octet-stream") { - val ext = message.text.substringAfterLast('.', "").lowercase() - return ext in VIDEO_EXTENSIONS - } - return false + return message.contentType.startsWith("video/") } private fun isImageMessage(message: Message): Boolean = message.contentType.startsWith("image/") diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt index 4260bd95..eabc8dd8 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt @@ -946,17 +946,21 @@ class ChatViewModel : ViewModel() { r.joinChannel(channelId, nodeId, joinKey, pubkey, addrs, addrCount) } - fun startVoiceRecording() { - val ch = _currentChannel.value ?: return - if (!audioRecorder.startRecording(ch.id)) { + private var recordingStopping = false + private var recordingTarget: ConversationTarget? = null + + fun startVoiceRecording(target: ConversationTarget) { + if (recordingStopping) { LogManager.addLog("WARN", "VM", "recording is still stopping"); return } + if (!audioRecorder.startRecording(target.nativeId)) { LogManager.addLog("ERROR", "VM", "voice recording failed to start") return } _isRecording.value = true + recordingTarget = target _isRecordingLocked.value = false recordingStartTimeMs = System.currentTimeMillis() _recordingDurationMs.value = 0 - LogManager.addLog("INFO", "VM", "voice recording started ch=${ch.id}") + LogManager.addLog("INFO", "VM", "voice recording started target=${target.nativeId}") } fun lockVoiceRecording() { @@ -964,42 +968,44 @@ class ChatViewModel : ViewModel() { LogManager.addLog("DEBUG", "VM", "voice recording locked") } - fun stopVoiceRecording(): Int { - if (!_isRecording.value) return 0 - val duration = audioRecorder.stopRecording() + fun stopVoiceRecording() { + if (!_isRecording.value || recordingStopping) return + recordingStopping = true _isRecording.value = false _isRecordingLocked.value = false _recordingDurationMs.value = 0 - val ch = _currentChannel.value - if (ch != null && duration >= 500) { viewModelScope.launch { - delay(500) - refreshMessages(ch.id) - refreshChannels() + try { + val duration = withContext(Dispatchers.IO) { audioRecorder.stopRecording() } + LogManager.addLog("INFO", "VM", "voice detached target=${recordingTarget?.nativeId} duration=$duration") + } finally { + recordingStopping = false + recordingTarget = null } } - LogManager.addLog("INFO", "VM", "voice recording stopped dur=${duration}ms") - return duration } fun cancelVoiceRecording() { - audioRecorder.cancelRecording() + if (recordingStopping) return + recordingStopping = true _isRecording.value = false _isRecordingLocked.value = false _recordingDurationMs.value = 0 - LogManager.addLog("INFO", "VM", "voice recording cancelled") + viewModelScope.launch { + try { withContext(Dispatchers.IO) { audioRecorder.cancelRecording() } } + finally { recordingStopping = false; recordingTarget = null } + } + LogManager.addLog("INFO", "VM", "voice recording cancelled target=${recordingTarget?.nativeId}") } fun sendVoiceMessage() { - val duration = stopVoiceRecording() - if (duration < 1000) { - LogManager.addLog("INFO", "VM", "voice too short (${duration}ms), discarded") - } + stopVoiceRecording() } - fun sendAttachment(filePath: String) { + fun sendAttachment(filePath: String, target: ConversationTarget, name: String = File(filePath).name) { val r = repo ?: return - val ch = _currentChannel.value ?: return + if (target is ConversationTarget.Pm) { NativeLib.dmSendFile(target.id, filePath, name); return } + val chId = target.id val ext = filePath.substringAfterLast('.', "").lowercase() if (ext in IMAGE_EXTENSIONS) { viewModelScope.launch { @@ -1010,29 +1016,28 @@ class ChatViewModel : ViewModel() { } val (w, h) = dims if (w > 0 && h > 0) { - LogManager.addLog("INFO", "VM", "sendAttachment(image) ch=${ch.id} ext=$ext ${w}x${h}") - r.sendPhoto(ch.id, filePath, w, h) + LogManager.addLog("INFO", "VM", "sendAttachment(image) ch=$chId ext=$ext ${w}x${h}") + r.sendPhoto(chId, filePath, w, h) } else { LogManager.addLog("WARN", "VM", "image decode failed, sent as file: $filePath") - r.sendAttachment(ch.id, filePath) + r.sendAttachment(chId, filePath) } delay(500) - refreshMessages(ch.id) + refreshMessages(chId) refreshChannels() } } else { - r.sendAttachment(ch.id, filePath) + r.sendAttachment(chId, filePath) viewModelScope.launch { delay(500) - refreshMessages(ch.id) + refreshMessages(chId) refreshChannels() } } } - fun sendVideoMessage(filePath: String) { + fun sendVideoMessage(filePath: String, target: ConversationTarget) { val r = repo ?: return - val ch = _currentChannel.value ?: return viewModelScope.launch { val meta = withContext(Dispatchers.IO) { var dur = 0; var w = 0; var h = 0 @@ -1049,11 +1054,8 @@ class ChatViewModel : ViewModel() { } Triple(dur, w, h) } - LogManager.addLog("INFO", "VM", "sendVideoMessage ch=${ch.id} dur=${meta.first} ${meta.second}x${meta.third}") - r.sendVideo(ch.id, filePath, meta.first, meta.second, meta.third) - delay(500) - refreshMessages(ch.id) - refreshChannels() + LogManager.addLog("INFO", "VM", "sendVideoMessage target=${target.nativeId} dur=${meta.first} ${meta.second}x${meta.third}") + r.sendVideo(target.nativeId, filePath, meta.first, meta.second, meta.third) } }