Skip to content

Commit

Permalink
fix: Pause AudioMessage on recording new #WPB-15852 (#3872)
Browse files Browse the repository at this point in the history
Co-authored-by: Mohamad Jaara <[email protected]>
  • Loading branch information
borichellow and MohamadJaara authored Feb 13, 2025
1 parent c4ba37b commit dc3887f
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,8 @@ class AudioFocusHelper @Inject constructor(private val audioManager: AudioManage
}
}

private val focusRequest by lazy {
AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
.setOnAudioFocusChangeListener(onAudioFocusChangeListener)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) setForceDucking(true)
}.build()
}
private val focusRequest by lazy { buildAudioFocusRequest() }
private val exclusiveFocusRequest by lazy { buildAudioFocusRequest(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) }

/**
* Requests the audio focus.
Expand All @@ -64,13 +59,29 @@ class AudioFocusHelper @Inject constructor(private val audioManager: AudioManage
return audioManager.requestAudioFocus(focusRequest) != AudioManager.AUDIOFOCUS_REQUEST_FAILED
}

/**
* Requests the exclusive audio focus (a temporary request of audio focus, anticipated to last a short amount of time,
* during which no other applications, or system components, should play anything).
* @return true in case if focus was granted (AudioMessage can be played), false - otherwise
*/
fun requestExclusive(): Boolean {
return audioManager.requestAudioFocus(exclusiveFocusRequest) != AudioManager.AUDIOFOCUS_REQUEST_FAILED
}

/**
* Abandon the audio focus.
*/
fun abandon() {
audioManager.abandonAudioFocusRequest(focusRequest)
}

/**
* Abandon the exclusive audio focus.
*/
fun abandonExclusive() {
audioManager.abandonAudioFocusRequest(focusRequest)
}

fun setListener(onPauseCurrentAudio: () -> Unit, onResumeCurrentAudio: () -> Unit) {
listener = object : PlayPauseListener {
override fun onPauseCurrentAudio() {
Expand All @@ -83,6 +94,13 @@ class AudioFocusHelper @Inject constructor(private val audioManager: AudioManage
}
}

private fun buildAudioFocusRequest(focusGain: Int = AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK): AudioFocusRequest =
AudioFocusRequest.Builder(focusGain)
.setOnAudioFocusChangeListener(onAudioFocusChangeListener)
.apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) setForceDucking(true)
}.build()

interface PlayPauseListener {
fun onPauseCurrentAudio()
fun onResumeCurrentAudio()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ package com.wire.android.media.audiomessage
import android.content.Context
import android.media.MediaPlayer
import androidx.core.net.toUri
import com.wire.android.di.ApplicationScope
import dagger.hilt.android.scopes.ViewModelScoped
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
Expand All @@ -30,14 +32,17 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject

@ViewModelScoped
class RecordAudioMessagePlayer @Inject constructor(
private val context: Context,
private val audioMediaPlayer: MediaPlayer,
private val wavesMaskHelper: AudioWavesMaskHelper
private val wavesMaskHelper: AudioWavesMaskHelper,
private val audioFocusHelper: AudioFocusHelper,
@ApplicationScope private val scope: CoroutineScope
) {
private var currentAudioFile: File? = null
private var audioState: AudioState = AudioState.DEFAULT
Expand All @@ -53,6 +58,11 @@ class RecordAudioMessagePlayer @Inject constructor(
seekToAudioPosition.tryEmit(0)
}
}

audioFocusHelper.setListener(
onPauseCurrentAudio = { scope.launch { pause() } },
onResumeCurrentAudio = { scope.launch { resumeAudio() } }
)
}

private val audioMessageStateUpdate =
Expand Down Expand Up @@ -152,7 +162,9 @@ class RecordAudioMessagePlayer @Inject constructor(
private suspend fun resumeOrPauseAudio() {
if (audioMediaPlayer.isPlaying) {
pause()
audioFocusHelper.abandon()
} else {
audioFocusHelper.request()
resumeAudio()
}
}
Expand All @@ -167,6 +179,7 @@ class RecordAudioMessagePlayer @Inject constructor(
context,
audioFile.toUri()
)
audioFocusHelper.request()
audioMediaPlayer.prepare()
audioMediaPlayer.seekTo(position)
audioMediaPlayer.start()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.appLogger
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.media.audiomessage.AudioFocusHelper
import com.wire.android.media.audiomessage.AudioMediaPlayingState
import com.wire.android.media.audiomessage.AudioState
import com.wire.android.media.audiomessage.AudioWavesMaskHelper
Expand Down Expand Up @@ -66,6 +67,7 @@ class RecordAudioViewModel @Inject constructor(
private val audioMediaRecorder: AudioMediaRecorder,
private val globalDataStore: GlobalDataStore,
private val audioWavesMaskHelper: AudioWavesMaskHelper,
private val audioFocusHelper: AudioFocusHelper,
private val dispatchers: DispatcherProvider,
private val kaliumFileSystem: KaliumFileSystem
) : ViewModel() {
Expand Down Expand Up @@ -160,6 +162,7 @@ class RecordAudioViewModel @Inject constructor(
infoMessage.emit(RecordAudioInfoMessageType.UnableToRecordAudioCall.uiText)
}
} else {
audioFocusHelper.requestExclusive()
viewModelScope.launch(dispatchers.default()) {
val assetSizeLimit = getAssetSizeLimit(false)
if (state.shouldApplyEffects && state.effectsOutputFile == null) {
Expand All @@ -182,6 +185,7 @@ class RecordAudioViewModel @Inject constructor(
}

fun stopRecording() {
audioFocusHelper.abandonExclusive()
viewModelScope.launch(dispatchers.default()) {
if (state.buttonState == RecordAudioButtonState.RECORDING) {
appLogger.i("[$tag] -> Stopping audioMediaRecorder")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.TestDispatcherProvider
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.framework.FakeKaliumFileSystem
import com.wire.android.media.audiomessage.AudioFocusHelper
import com.wire.android.media.audiomessage.AudioState
import com.wire.android.media.audiomessage.AudioWavesMaskHelper
import com.wire.android.media.audiomessage.RecordAudioMessagePlayer
Expand Down Expand Up @@ -286,7 +287,7 @@ class RecordAudioViewModelTest {
fun `given user recorded an audio, when discarding the audio, then file is deleted`() =
runTest {
// given
val (_, viewModel) = Arrangement()
val (arrangement, viewModel) = Arrangement()
.withFilterEnabled(false)
.arrange()

Expand All @@ -309,6 +310,9 @@ class RecordAudioViewModelTest {
viewModel.state.originalOutputFile
)
}

verify(exactly = 1) { arrangement.audioFocusHelper.requestExclusive() }
verify(exactly = 2) { arrangement.audioFocusHelper.abandonExclusive() } // 1 before start recording, 1 after
}

@Test
Expand All @@ -333,7 +337,7 @@ class RecordAudioViewModelTest {
fun `given start recording failed, when recording audio, then info message is shown`() =
runTest {
// given
val (_, viewModel) = Arrangement()
val (arrangement, viewModel) = Arrangement()
.withStartRecordingFailed()
.withFilterEnabled(false)
.arrange()
Expand All @@ -344,6 +348,8 @@ class RecordAudioViewModelTest {
// then
assertEquals(RecordAudioButtonState.ENABLED, viewModel.state.buttonState)
assertEquals(RecordAudioInfoMessageType.UnableToRecordAudioError.uiText, awaitItem())

verify(exactly = 1) { arrangement.audioFocusHelper.requestExclusive() }
}
}

Expand All @@ -360,6 +366,7 @@ class RecordAudioViewModelTest {
val dispatchers = TestDispatcherProvider()
val fakeKaliumFileSystem = FakeKaliumFileSystem()
val audioWavesMaskHelper = mockk<AudioWavesMaskHelper>()
val audioFocusHelper = mockk<AudioFocusHelper>()

val viewModel by lazy {
RecordAudioViewModel(
Expand All @@ -373,7 +380,8 @@ class RecordAudioViewModelTest {
globalDataStore = globalDataStore,
dispatchers = dispatchers,
audioWavesMaskHelper = audioWavesMaskHelper,
kaliumFileSystem = fakeKaliumFileSystem
kaliumFileSystem = fakeKaliumFileSystem,
audioFocusHelper = audioFocusHelper
)
}

Expand Down Expand Up @@ -409,6 +417,9 @@ class RecordAudioViewModelTest {

every { audioWavesMaskHelper.getWaveMask(any<File>()) } returns listOf()
every { audioWavesMaskHelper.getWaveMask(any<Path>()) } returns listOf()

every { audioFocusHelper.requestExclusive() } returns true
every { audioFocusHelper.abandonExclusive() } returns Unit
}

fun withEstablishedCall() = apply {
Expand Down

0 comments on commit dc3887f

Please sign in to comment.