From 286ad3f627f4acce7e63c0071407d80f0c6f1a88 Mon Sep 17 00:00:00 2001 From: boris Date: Tue, 26 Nov 2024 12:30:39 +0200 Subject: [PATCH] fix: Listen few audios at the same time [WPB-11180] (#3639) --- .../ConversationAudioMessagePlayer.kt | 64 +++++++++++++++---- .../messages/ConversationMessagesViewModel.kt | 7 +- .../ConversationAudioMessagePlayerTest.kt | 34 ++++++++-- ...onversationMessagesViewModelArrangement.kt | 8 ++- 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt index 8fb90a99f6b..3f6c08ed1be 100644 --- a/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt +++ b/app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt @@ -21,9 +21,11 @@ import android.content.Context import android.media.MediaPlayer import android.media.MediaPlayer.SEEK_CLOSEST_SYNC import android.net.Uri +import com.wire.android.di.KaliumCoreLogic +import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase import com.wire.kalium.logic.feature.asset.MessageAssetResult +import com.wire.kalium.logic.feature.session.CurrentSessionResult import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay @@ -34,14 +36,44 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch import javax.inject.Inject +import javax.inject.Singleton -class ConversationAudioMessagePlayer +@Singleton +class ConversationAudioMessagePlayerProvider @Inject constructor( private val context: Context, private val audioMediaPlayer: MediaPlayer, - private val getMessageAsset: GetMessageAssetUseCase + @KaliumCoreLogic private val coreLogic: CoreLogic, +) { + private var player: ConversationAudioMessagePlayer? = null + private var usageCount: Int = 0 + + @Synchronized + fun provide(): ConversationAudioMessagePlayer { + val player = player ?: ConversationAudioMessagePlayer(context, audioMediaPlayer, coreLogic).also { player = it } + usageCount++ + + return player + } + + @Synchronized + fun onCleared() { + usageCount-- + if (usageCount <= 0) { + player?.close() + player = null + } + } +} + +class ConversationAudioMessagePlayer +internal constructor( + private val context: Context, + private val audioMediaPlayer: MediaPlayer, + @KaliumCoreLogic private val coreLogic: CoreLogic, ) { private companion object { const val UPDATE_POSITION_INTERVAL_IN_MS = 1000L @@ -137,7 +169,7 @@ class ConversationAudioMessagePlayer } audioMessageStateHistory - } + }.onStart { emit(audioMessageStateHistory) } private var currentAudioMessageId: String? = null @@ -169,10 +201,10 @@ class ConversationAudioMessagePlayer } private suspend fun stopCurrentlyPlayingAudioMessage() { - if (currentAudioMessageId != null) { - val currentAudioState = audioMessageStateHistory[currentAudioMessageId] + currentAudioMessageId?.let { + val currentAudioState = audioMessageStateHistory[it] if (currentAudioState?.audioMediaPlayingState != AudioMediaPlayingState.Fetching) { - stop(currentAudioMessageId!!) + stop(it) } } } @@ -194,6 +226,9 @@ class ConversationAudioMessagePlayer coroutineScope { launch { + val currentAccountResult = coreLogic.getGlobalScope().session.currentSession() + if (currentAccountResult is CurrentSessionResult.Failure) return@launch + audioMessageStateUpdate.emit( AudioMediaPlayerStateUpdate.AudioMediaPlayingStateUpdate( messageId, @@ -201,7 +236,12 @@ class ConversationAudioMessagePlayer ) ) - when (val result = getMessageAsset(conversationId, messageId).await()) { + val assetMessage = coreLogic + .getSessionScope((currentAccountResult as CurrentSessionResult.Success).accountInfo.userId) + .messages + .getAssetMessage(conversationId, messageId) + + when (val result = assetMessage.await()) { is MessageAssetResult.Success -> { audioMessageStateUpdate.emit( AudioMediaPlayerStateUpdate.AudioMediaPlayingStateUpdate( @@ -219,9 +259,7 @@ class ConversationAudioMessagePlayer ) audioMediaPlayer.prepare() - if (position != null) { - audioMediaPlayer.seekTo(position) - } + if (position != null) audioMediaPlayer.seekTo(position) audioMediaPlayer.start() @@ -292,7 +330,7 @@ class ConversationAudioMessagePlayer ) } - fun close() { - audioMediaPlayer.release() + internal fun close() { + audioMediaPlayer.reset() } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt index d7ffb2d2165..84aef9ce71f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModel.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import com.wire.android.R import com.wire.android.appLogger -import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayerProvider import com.wire.android.model.SnackBarMessage import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.home.conversations.ConversationNavArgs @@ -98,7 +98,7 @@ class ConversationMessagesViewModel @Inject constructor( private val getMessageForConversation: GetMessagesForConversationUseCase, private val toggleReaction: ToggleReactionUseCase, private val resetSession: ResetSessionUseCase, - private val conversationAudioMessagePlayer: ConversationAudioMessagePlayer, + private val conversationAudioMessagePlayerProvider: ConversationAudioMessagePlayerProvider, private val getConversationUnreadEventsCount: GetConversationUnreadEventsCountUseCase, private val clearUsersTypingEvents: ClearUsersTypingEventsUseCase, private val getSearchedConversationMessagePosition: GetSearchedConversationMessagePositionUseCase, @@ -108,6 +108,7 @@ class ConversationMessagesViewModel @Inject constructor( private val conversationNavArgs: ConversationNavArgs = savedStateHandle.navArgs() val conversationId: QualifiedID = conversationNavArgs.conversationId private val searchedMessageIdNavArgs: String? = conversationNavArgs.searchedMessageId + private val conversationAudioMessagePlayer = conversationAudioMessagePlayerProvider.provide() var conversationViewState by mutableStateOf( ConversationMessagesViewState( @@ -436,7 +437,7 @@ class ConversationMessagesViewModel @Inject constructor( override fun onCleared() { super.onCleared() - conversationAudioMessagePlayer.close() + conversationAudioMessagePlayerProvider.onCleared() } private companion object { diff --git a/app/src/test/kotlin/com/wire/android/media/ConversationAudioMessagePlayerTest.kt b/app/src/test/kotlin/com/wire/android/media/ConversationAudioMessagePlayerTest.kt index 57c51fcb7c9..d761d47ca4c 100644 --- a/app/src/test/kotlin/com/wire/android/media/ConversationAudioMessagePlayerTest.kt +++ b/app/src/test/kotlin/com/wire/android/media/ConversationAudioMessagePlayerTest.kt @@ -25,9 +25,12 @@ import com.wire.android.framework.FakeKaliumFileSystem import com.wire.android.media.audiomessage.AudioMediaPlayingState import com.wire.android.media.audiomessage.AudioState import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.id.ConversationId -import com.wire.kalium.logic.feature.asset.GetMessageAssetUseCase +import com.wire.kalium.logic.data.user.UserId import com.wire.kalium.logic.feature.asset.MessageAssetResult +import com.wire.kalium.logic.feature.session.CurrentSessionResult import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every @@ -45,11 +48,14 @@ class ConversationAudioMessagePlayerTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withAudioMediaPlayerReturningTotalTime(1000) .withSuccessFullAssetFetch() + .withCurrentSession() .arrange() val testAudioMessageId = "some-dummy-message-id" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), testAudioMessageId @@ -95,6 +101,7 @@ class ConversationAudioMessagePlayerTest { fun givenTheSuccessFullAssetFetch_whenPlayingTheSameMessageIdTwiceSequentially_thenEmitStatesAsExpected() = runTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withSuccessFullAssetFetch() + .withCurrentSession() .withAudioMediaPlayerReturningTotalTime(1000) .withMediaPlayerPlaying() .arrange() @@ -102,6 +109,8 @@ class ConversationAudioMessagePlayerTest { val testAudioMessageId = "some-dummy-message-id" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() // playing first time conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), @@ -161,6 +170,7 @@ class ConversationAudioMessagePlayerTest { runTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withSuccessFullAssetFetch() + .withCurrentSession() .withAudioMediaPlayerReturningTotalTime(1000) .arrange() @@ -168,6 +178,8 @@ class ConversationAudioMessagePlayerTest { val secondAudioMessageId = "some-dummy-message-id2" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() // playing first audio message conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), @@ -242,6 +254,7 @@ class ConversationAudioMessagePlayerTest { runTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withSuccessFullAssetFetch() + .withCurrentSession() .withAudioMediaPlayerReturningTotalTime(1000) .arrange() @@ -249,6 +262,8 @@ class ConversationAudioMessagePlayerTest { val secondAudioMessageId = "some-dummy-message-id2" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() // playing first audio message conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), @@ -366,6 +381,7 @@ class ConversationAudioMessagePlayerTest { runTest { val (arrangement, conversationAudioMessagePlayer) = Arrangement() .withSuccessFullAssetFetch() + .withCurrentSession() .withAudioMediaPlayerReturningTotalTime(1000) .withMediaPlayerPlaying() .arrange() @@ -373,6 +389,8 @@ class ConversationAudioMessagePlayerTest { val testAudioMessageId = "some-dummy-message-id" conversationAudioMessagePlayer.observableAudioMessagesState.test { + // skip first emit from onStart + awaitItem() // playing first time conversationAudioMessagePlayer.playAudio( ConversationId("some-dummy-value", "some.dummy.domain"), @@ -454,7 +472,7 @@ class Arrangement { lateinit var context: Context @MockK - lateinit var getMessageAssetUseCase: GetMessageAssetUseCase + lateinit var coreLogic: CoreLogic @MockK lateinit var mediaPlayer: MediaPlayer @@ -463,7 +481,7 @@ class Arrangement { ConversationAudioMessagePlayer( context, mediaPlayer, - getMessageAssetUseCase, + coreLogic, ) } @@ -471,8 +489,16 @@ class Arrangement { MockKAnnotations.init(this, relaxed = true) } + fun withCurrentSession() = apply { + coEvery { coreLogic.getGlobalScope().session.currentSession.invoke() } returns CurrentSessionResult.Success( + AccountInfo.Valid(UserId("some-user-value", "some.user.domain")) + ) + } + fun withSuccessFullAssetFetch() = apply { - coEvery { getMessageAssetUseCase.invoke(any(), any()) } returns CompletableDeferred( + coEvery { + coreLogic.getSessionScope(any()).messages.getAssetMessage.invoke(any(), any()) + } returns CompletableDeferred( MessageAssetResult.Success( decodedAssetPath = FakeKaliumFileSystem().selfUserAvatarPath(), assetSize = 0, diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt index 491c2fd2e56..bc48d1c4fe4 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/messages/ConversationMessagesViewModelArrangement.kt @@ -24,6 +24,7 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.media.audiomessage.AudioState import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer +import com.wire.android.media.audiomessage.ConversationAudioMessagePlayerProvider import com.wire.android.ui.home.conversations.ConversationNavArgs import com.wire.android.ui.home.conversations.model.AssetBundle import com.wire.android.ui.home.conversations.model.UIMessage @@ -96,6 +97,9 @@ class ConversationMessagesViewModelArrangement { @MockK lateinit var conversationAudioMessagePlayer: ConversationAudioMessagePlayer + @MockK + lateinit var conversationAudioMessagePlayerProvider: ConversationAudioMessagePlayerProvider + @MockK lateinit var getConversationUnreadEventsCount: GetConversationUnreadEventsCountUseCase @@ -124,7 +128,7 @@ class ConversationMessagesViewModelArrangement { getMessagesForConversationUseCase, toggleReaction, resetSession, - conversationAudioMessagePlayer, + conversationAudioMessagePlayerProvider, getConversationUnreadEventsCount, clearUsersTypingEvents, getSearchedConversationMessagePosition, @@ -143,6 +147,8 @@ class ConversationMessagesViewModelArrangement { coEvery { getConversationUnreadEventsCount(any()) } returns GetConversationUnreadEventsCountUseCase.Result.Success(0L) coEvery { updateAssetMessageDownloadStatus(any(), any(), any()) } returns UpdateTransferStatusResult.Success coEvery { clearUsersTypingEvents() } returns Unit + every { conversationAudioMessagePlayerProvider.provide() } returns conversationAudioMessagePlayer + every { conversationAudioMessagePlayerProvider.onCleared() } returns Unit coEvery { getSearchedConversationMessagePosition(any(), any()) } returns GetSearchedConversationMessagePositionUseCase.Result.Success(position = 0)