From b57e48fe20a00086d7ad19f3de9fd385f0e4f06f Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Sun, 12 Apr 2026 14:17:03 +0500 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B0?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BF=D0=B8=D1=81=D0=B8=20?= =?UTF-8?q?=D0=93=D0=A1=20=E2=80=94=20race=20condition=20=D0=B2=20startVoi?= =?UTF-8?q?ceRecording=20+=20=D1=83=D1=82=D0=B5=D1=87=D0=BA=D0=B0=20isVoic?= =?UTF-8?q?eRecordTransitioning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause 1: startVoiceRecording() проверял только isVoiceRecording, но isVoiceRecording=true ставился через 192ms в scope.launch. При быстром двойном тапе два MediaRecorder создавались, первый терялся (утечка). Фикс: добавлен guard на isVoiceRecordTransitioning и voiceRecorder!=null. Root cause 2: isVoiceRecordTransitioning=true ставился перед scope.launch, но если launch крашился или composable disposed, transitioning навсегда оставался true — gesture guard блокировал все записи до перезапуска. Фикс: try/catch в launch + reset в DisposableEffect. Root cause 3: DisposableEffect проверял только isVoiceRecording, но не voiceRecorder!=null — если recorder создан но isVoiceRecording ещё false, recorder не освобождался при dispose. Фикс: проверка voiceRecorder!=null в dispose. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ui/chats/input/ChatDetailInput.kt | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt index e74fdf7..1714a77 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/input/ChatDetailInput.kt @@ -1180,7 +1180,7 @@ fun MessageInputBar( } fun startVoiceRecording() { - if (isVoiceRecording) return + if (isVoiceRecording || isVoiceRecordTransitioning || voiceRecorder != null) return inputJumpLog( "startVoiceRecording begin mode=$recordMode state=$recordUiState kb=$isKeyboardVisible emojiBox=${coordinator.isEmojiBoxVisible} " + "emojiPicker=$showEmojiPicker panelH=$inputPanelHeightPx normalH=$normalInputRowHeightPx" @@ -1227,20 +1227,25 @@ fun MessageInputBar( ) scope.launch { - repeat(12) { - if (!isKeyboardVisible && !coordinator.isEmojiBoxVisible) return@repeat - delay(16) + try { + repeat(12) { + if (!isKeyboardVisible && !coordinator.isEmojiBoxVisible) return@repeat + delay(16) + } + isVoiceRecording = true + isVoiceRecordTransitioning = false + if (recordUiState == RecordUiState.PRESSING || recordUiState == RecordUiState.IDLE) { + setRecordUiState(RecordUiState.RECORDING, "voice-recorder-started") + } + inputJumpLog( + "startVoiceRecording ui-enter mode=$recordMode state=$recordUiState voice=$isVoiceRecording kb=$isKeyboardVisible " + + "emojiBox=${coordinator.isEmojiBoxVisible} transitioning=$isVoiceRecordTransitioning " + + "panelH=$inputPanelHeightPx recH=$recordingInputRowHeightPx" + ) + } catch (e: Exception) { + isVoiceRecordTransitioning = false + inputJumpLog("startVoiceRecording launch failed: ${e.message}") } - isVoiceRecording = true - isVoiceRecordTransitioning = false - if (recordUiState == RecordUiState.PRESSING || recordUiState == RecordUiState.IDLE) { - setRecordUiState(RecordUiState.RECORDING, "voice-recorder-started") - } - inputJumpLog( - "startVoiceRecording ui-enter mode=$recordMode state=$recordUiState voice=$isVoiceRecording kb=$isKeyboardVisible " + - "emojiBox=${coordinator.isEmojiBoxVisible} transitioning=$isVoiceRecordTransitioning " + - "panelH=$inputPanelHeightPx recH=$recordingInputRowHeightPx" - ) } } catch (_: Exception) { isVoiceRecordTransitioning = false @@ -1420,8 +1425,9 @@ fun MessageInputBar( DisposableEffect(Unit) { onDispose { pendingRecordAfterPermission = false + isVoiceRecordTransitioning = false resetGestureState() - if (isVoiceRecording) { + if (isVoiceRecording || voiceRecorder != null) { stopVoiceRecording(send = false) } else { setRecordUiState(RecordUiState.IDLE, "dispose")