diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt index b7c640e..51471a9 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -945,19 +945,32 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) { */ fun startRecording() { if (recording != null) return - val started = runCatching { VoiceNote.start(getApplication().cacheDir) } - started.onFailure { failure -> - update { it.copy(error = "Could not start recording: ${failure.message}") } - return - } - recording = started.getOrNull() - // A visible timer, because a recorder with no feedback is one people - // talk into after it has already stopped. - recordingTicker = viewModelScope.launch { - while (recording != null) { - val seconds = recording?.elapsedSeconds - update { it.copy(openChat = it.openChat?.copy(recordingSeconds = seconds)) } - kotlinx.coroutines.delay(200) + viewModelScope.launch { + // 🔴 **Off the main thread.** `MediaRecorder.prepare()` and + // `start()` acquire the microphone, and acquiring hardware blocks — + // on a device whose audio route is busy that is the UI thread + // stalled for as long as it takes. `stopAndSendRecording` below + // already went through Dispatchers.IO for exactly this reason; + // starting did not, and starting is the half that has to wait for + // the route to come up. The iOS recorder had the same bug and + // froze its main thread for SIXTY SECONDS on a simulator. + val started = withContext(Dispatchers.IO) { + runCatching { VoiceNote.start(getApplication().cacheDir) } + } + val begun = started.getOrElse { failure -> + update { it.copy(error = "Could not start recording: ${failure.message}") } + return@launch + } + recording = begun + + // A visible timer, because a recorder with no feedback is one + // people talk into after it has already stopped. + recordingTicker = viewModelScope.launch { + while (recording != null) { + val seconds = recording?.elapsedSeconds + update { it.copy(openChat = it.openChat?.copy(recordingSeconds = seconds)) } + kotlinx.coroutines.delay(200) + } } } } @@ -966,9 +979,16 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) { fun cancelRecording() { recordingTicker?.cancel() recordingTicker = null - recording?.cancel() + // ⚠️ Releasing the recorder blocks too, so it goes the same way as + // starting and stopping. The UI state is cleared immediately — the + // user pressed Cancel and should not watch a bar linger while the + // hardware is handed back. + val discarded = recording recording = null update { it.copy(openChat = it.openChat?.copy(recordingSeconds = null)) } + if (discarded != null) { + viewModelScope.launch { withContext(Dispatchers.IO) { discarded.cancel() } } + } } /**