Android acquired the microphone on the UI thread too
Porting the iOS recorder fix back the other way, and the same bug was here. `MediaRecorder.prepare()` and `start()` acquire the microphone, and acquiring hardware blocks. `startRecording` was a plain function called straight from the UI, so that happened on the main thread — as did `File.createTempFile` beside it. `cancelRecording` released the recorder the same way. ⚠️ **The tell was the inconsistency.** `stopAndSendRecording` already went through `withContext(Dispatchers.IO)`, so someone had thought about this for stopping and not for starting — and starting is the half that waits for the audio route to come up. On iOS the identical mistake froze the main thread for SIXTY SECONDS on a simulator; here it did not misbehave on the emulator, which is not the same as being correct on a device whose audio route is busy. All three paths now agree. The UI state on cancel is cleared immediately rather than after the hardware is handed back, because the user pressed Cancel and should not watch the bar linger. Verified on the emulator: recording still starts (timer ticking at 8.4s), cancel still stops it and the composer returns, app alive throughout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ea338ea81a
commit
6527f76151
@@ -945,19 +945,32 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
*/
|
||||
fun startRecording() {
|
||||
if (recording != null) return
|
||||
val started = runCatching { VoiceNote.start(getApplication<Application>().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<Application>().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() } }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user