Merge pull request #6109 from kknappe/fix/start-voice-recording-when-permission-is-granted

Fix voice message recording not starting after permission is granted
This commit is contained in:
Benoit Marty 2026-02-09 09:28:58 +01:00 committed by GitHub
commit 1c28a26a99
2 changed files with 24 additions and 5 deletions

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -69,6 +70,7 @@ class DefaultVoiceMessageComposerPresenter(
} }
private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO) private val permissionsPresenter = permissionsPresenterFactory.create(Manifest.permission.RECORD_AUDIO)
private var pendingEvent: VoiceMessageRecorderEvent.Start? = null
private val mediaSender = mediaSenderFactory.create(timelineMode) private val mediaSender = mediaSenderFactory.create(timelineMode)
@Composable @Composable
@ -77,8 +79,7 @@ class DefaultVoiceMessageComposerPresenter(
val recorderState by voiceRecorder.state.collectAsState(initial = VoiceRecorderState.Idle) val recorderState by voiceRecorder.state.collectAsState(initial = VoiceRecorderState.Idle)
val playerState by player.state.collectAsState(initial = VoiceMessageComposerPlayer.State.Initial) val playerState by player.state.collectAsState(initial = VoiceMessageComposerPlayer.State.Initial)
val keepScreenOn by remember { derivedStateOf { recorderState is VoiceRecorderState.Recording } } val keepScreenOn by remember { derivedStateOf { recorderState is VoiceRecorderState.Recording } }
val permissionState by rememberUpdatedState(permissionsPresenter.present())
val permissionState = permissionsPresenter.present()
var isSending by remember { mutableStateOf(false) } var isSending by remember { mutableStateOf(false) }
var showSendFailureDialog by remember { mutableStateOf(false) } var showSendFailureDialog by remember { mutableStateOf(false) }
@ -88,6 +89,15 @@ class DefaultVoiceMessageComposerPresenter(
player.setMedia(recording.file.path) player.setMedia(recording.file.path)
} }
LaunchedEffect(permissionState.permissionGranted) {
if (permissionState.permissionGranted) {
pendingEvent?.let {
localCoroutineScope.startRecording()
pendingEvent = null
}
}
}
fun handleLifecycleEvent(event: Lifecycle.Event) { fun handleLifecycleEvent(event: Lifecycle.Event) {
when (event) { when (event) {
Lifecycle.Event.ON_PAUSE -> { Lifecycle.Event.ON_PAUSE -> {
@ -102,6 +112,7 @@ class DefaultVoiceMessageComposerPresenter(
} }
fun handleVoiceMessageRecorderEvent(event: VoiceMessageRecorderEvent) { fun handleVoiceMessageRecorderEvent(event: VoiceMessageRecorderEvent) {
pendingEvent = null
when (event) { when (event) {
VoiceMessageRecorderEvent.Start -> { VoiceMessageRecorderEvent.Start -> {
Timber.v("Voice message record button pressed") Timber.v("Voice message record button pressed")
@ -111,6 +122,7 @@ class DefaultVoiceMessageComposerPresenter(
} }
else -> { else -> {
Timber.i("Voice message permission needed") Timber.i("Voice message permission needed")
pendingEvent = VoiceMessageRecorderEvent.Start
permissionState.eventSink(PermissionsEvent.RequestPermissions) permissionState.eventSink(PermissionsEvent.RequestPermissions)
} }
} }

View file

@ -522,7 +522,9 @@ class DefaultVoiceMessageComposerPresenterTest {
permissionsPresenter.setPermissionGranted() permissionsPresenter.setPermissionGranted()
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
val finalState = awaitItem() advanceUntilIdle()
val finalState = expectMostRecentItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(stopped = 1, started = 1) voiceRecorder.assertCalls(stopped = 1, started = 1)
@ -547,14 +549,16 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(it.showPermissionRationaleDialog).isTrue() assertThat(it.showPermissionRationaleDialog).isTrue()
it.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale) it.eventSink(VoiceMessageComposerEvent.AcceptPermissionRationale)
} }
skipItems(1)
// Dialog is hidden, user accepts permissions // Dialog is hidden, user accepts permissions
assertThat(awaitItem().showPermissionRationaleDialog).isFalse() assertThat(awaitItem().showPermissionRationaleDialog).isFalse()
// Permission is granted, recording starts automatically
permissionsPresenter.setPermissionGranted() permissionsPresenter.setPermissionGranted()
advanceUntilIdle()
awaitItem().eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) val finalState = expectMostRecentItem()
val finalState = awaitItem()
assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE) assertThat(finalState.voiceMessageState).isEqualTo(RECORDING_STATE)
voiceRecorder.assertCalls(started = 1) voiceRecorder.assertCalls(started = 1)
@ -579,12 +583,14 @@ class DefaultVoiceMessageComposerPresenterTest {
assertThat(it.showPermissionRationaleDialog).isTrue() assertThat(it.showPermissionRationaleDialog).isTrue()
it.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale) it.eventSink(VoiceMessageComposerEvent.DismissPermissionsRationale)
} }
skipItems(1)
// Dialog is hidden, user tries to record again // Dialog is hidden, user tries to record again
awaitItem().also { awaitItem().also {
assertThat(it.showPermissionRationaleDialog).isFalse() assertThat(it.showPermissionRationaleDialog).isFalse()
it.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start)) it.eventSink(VoiceMessageComposerEvent.RecorderEvent(VoiceMessageRecorderEvent.Start))
} }
skipItems(1)
// Dialog is shown once again // Dialog is shown once again
val finalState = awaitItem().also { val finalState = awaitItem().also {
@ -593,6 +599,7 @@ class DefaultVoiceMessageComposerPresenterTest {
} }
voiceRecorder.assertCalls(started = 0) voiceRecorder.assertCalls(started = 0)
cancelAndIgnoreRemainingEvents()
testPauseAndDestroy(finalState) testPauseAndDestroy(finalState)
} }
} }