Skip to content

Commit 54275c2

Browse files
refactor: build waveform after fetching audio asset and update waveform colors [WPB-22118] (#4474)
Co-authored-by: Mohamad Jaara <[email protected]>
1 parent c14ed40 commit 54275c2

File tree

18 files changed

+158
-292
lines changed

18 files changed

+158
-292
lines changed

app/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,6 @@ dependencies {
270270
implementation(libs.aboutLibraries.compose.core)
271271
implementation(libs.aboutLibraries.compose.m3)
272272
implementation(libs.compose.qr.code)
273-
implementation(libs.audio.amplituda)
274273
implementation(libs.enterprise.feedback)
275274

276275
// screenshot testing

app/src/main/kotlin/com/wire/android/media/audiomessage/AudioMessageViewModel.kt

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,14 @@ import com.wire.android.di.ScopedArgs
2828
import com.wire.android.di.ViewModelScopedPreview
2929
import com.wire.android.di.scopedArgs
3030
import com.wire.kalium.logic.data.id.ConversationId
31+
import com.wire.kalium.logic.data.message.AssetContent.AssetMetadata
32+
import com.wire.kalium.logic.data.message.MessageContent
33+
import com.wire.kalium.logic.feature.message.ObserveMessageByIdUseCase
3134
import dagger.hilt.android.lifecycle.HiltViewModel
3235
import kotlinx.coroutines.flow.collectLatest
3336
import kotlinx.coroutines.flow.distinctUntilChanged
37+
import kotlinx.coroutines.flow.firstOrNull
38+
import kotlinx.coroutines.flow.map
3439
import kotlinx.coroutines.flow.mapNotNull
3540
import kotlinx.coroutines.launch
3641
import kotlinx.serialization.Serializable
@@ -47,6 +52,7 @@ interface AudioMessageViewModel {
4752
@HiltViewModel
4853
class AudioMessageViewModelImpl @Inject constructor(
4954
private val audioMessagePlayer: ConversationAudioMessagePlayer,
55+
private val observeMessageById: ObserveMessageByIdUseCase,
5056
savedStateHandle: SavedStateHandle,
5157
) : ViewModel(), AudioMessageViewModel {
5258

@@ -58,7 +64,8 @@ class AudioMessageViewModelImpl @Inject constructor(
5864
init {
5965
observeAudioState()
6066
observeAudioSpeed()
61-
initWavesMask()
67+
preloadAudioMessage()
68+
fetchWavesMask()
6269
}
6370

6471
private fun observeAudioState() {
@@ -84,9 +91,26 @@ class AudioMessageViewModelImpl @Inject constructor(
8491
}
8592
}
8693

87-
private fun initWavesMask() {
94+
private fun preloadAudioMessage() {
8895
viewModelScope.launch {
89-
audioMessagePlayer.getOrBuildWavesMask(args.conversationId, args.messageId)
96+
// calls preload to initially fetch the audio asset data to be ready and schedule waves mask generation if needed
97+
audioMessagePlayer.preloadAudioMessage(args.conversationId, args.messageId)
98+
}
99+
}
100+
101+
private fun fetchWavesMask() {
102+
viewModelScope.launch {
103+
observeMessageById(args.conversationId, args.messageId)
104+
.map {
105+
(it as? ObserveMessageByIdUseCase.Result.Success)?.message?.content?.let {
106+
(it as? MessageContent.Asset)?.value?.metadata?.let {
107+
(it as? AssetMetadata.Audio)?.normalizedLoudness?.toWavesMask()
108+
}
109+
}
110+
}
111+
.distinctUntilChanged()
112+
.firstOrNull { it != null } // wait for the first non-null value
113+
.let { state = state.copy(wavesMask = it) }
90114
}
91115
}
92116

@@ -125,4 +149,5 @@ data class AudioMessageArgs(
125149
data class AudioMessageState(
126150
val audioSpeed: AudioSpeed = AudioSpeed.NORMAL,
127151
val audioState: AudioState = AudioState.DEFAULT,
152+
val wavesMask: List<Int>? = null
128153
)

app/src/main/kotlin/com/wire/android/media/audiomessage/AudioState.kt

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,16 @@ package com.wire.android.media.audiomessage
1919

2020
import androidx.annotation.StringRes
2121
import com.wire.android.R
22-
import com.wire.android.media.audiomessage.ConversationAudioMessagePlayer.MessageIdWrapper
2322
import com.wire.android.util.ui.UIText
2423
import com.wire.kalium.logic.data.id.ConversationId
2524

26-
data class AudioMessagesData(
27-
val statesHistory: Map<MessageIdWrapper, AudioState>,
28-
val playingMessage: PlayingAudioMessage
29-
)
30-
3125
data class AudioState(
3226
val audioMediaPlayingState: AudioMediaPlayingState,
3327
val currentPositionInMs: Int,
3428
val totalTimeInMs: TotalTimeInMs,
35-
val wavesMask: List<Int>?
3629
) {
3730
companion object {
38-
val DEFAULT = AudioState(AudioMediaPlayingState.Stopped, 0, TotalTimeInMs.NotKnown, null)
31+
val DEFAULT = AudioState(AudioMediaPlayingState.Stopped, 0, TotalTimeInMs.NotKnown)
3932
}
4033

4134
// if the back-end returned the total time, we use that, in case it didn't we use what we get from
@@ -142,12 +135,6 @@ sealed class AudioMediaPlayerStateUpdate(
142135
override val messageId: String,
143136
val totalTimeInMs: Int
144137
) : AudioMediaPlayerStateUpdate(conversationId, messageId)
145-
146-
data class WaveMaskUpdate(
147-
override val conversationId: ConversationId,
148-
override val messageId: String,
149-
val waveMask: List<Int>?
150-
) : AudioMediaPlayerStateUpdate(conversationId, messageId)
151138
}
152139

153140
sealed class RecordAudioMediaPlayerStateUpdate {
@@ -162,8 +149,4 @@ sealed class RecordAudioMediaPlayerStateUpdate {
162149
data class TotalTimeUpdate(
163150
val totalTimeInMs: Int
164151
) : RecordAudioMediaPlayerStateUpdate()
165-
166-
data class WaveMaskUpdate(
167-
val waveMask: List<Int>
168-
) : RecordAudioMediaPlayerStateUpdate()
169152
}

app/src/main/kotlin/com/wire/android/media/audiomessage/ConversationAudioMessagePlayer.kt

Lines changed: 2 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,8 @@ import com.wire.android.util.extension.intervalFlow
3030
import com.wire.android.util.ui.UIText
3131
import com.wire.kalium.logic.CoreLogic
3232
import com.wire.kalium.logic.data.id.ConversationId
33-
import com.wire.kalium.logic.data.message.AssetContent.AssetMetadata
34-
import com.wire.kalium.logic.data.message.MessageContent
3533
import com.wire.kalium.logic.data.user.UserId
36-
import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder
3734
import com.wire.kalium.logic.feature.asset.MessageAssetResult
38-
import com.wire.kalium.logic.feature.message.GetMessageByIdUseCase
3935
import com.wire.kalium.logic.feature.message.GetNextAudioMessageInConversationUseCase
4036
import com.wire.kalium.logic.feature.message.GetSenderNameByMessageIdUseCase
4137
import com.wire.kalium.logic.feature.session.CurrentSessionResult
@@ -62,7 +58,6 @@ import kotlinx.coroutines.launch
6258
import kotlinx.coroutines.sync.Mutex
6359
import kotlinx.coroutines.sync.withLock
6460
import kotlinx.coroutines.withContext
65-
import okio.Path
6661
import javax.inject.Inject
6762
import javax.inject.Singleton
6863

@@ -72,7 +67,6 @@ class ConversationAudioMessagePlayer
7267
@Inject constructor(
7368
@ApplicationContext private val context: Context,
7469
private val audioMediaPlayer: MediaPlayer,
75-
private val audioNormalizedLoudnessBuilder: AudioNormalizedLoudnessBuilder,
7670
private val servicesManager: Lazy<ServicesManager>,
7771
private val audioFocusHelper: AudioFocusHelper,
7872
@KaliumCoreLogic private val coreLogic: CoreLogic,
@@ -194,15 +188,6 @@ class ConversationAudioMessagePlayer
194188
)
195189
}
196190
}
197-
198-
is AudioMediaPlayerStateUpdate.WaveMaskUpdate -> {
199-
audioMessageStateHistory = audioMessageStateHistory.toMutableMap().apply {
200-
put(
201-
messageIdKey,
202-
currentState.copy(wavesMask = audioMessageStateUpdate.waveMask)
203-
)
204-
}
205-
}
206191
}
207192

208193
audioMessageStateHistory
@@ -311,12 +296,6 @@ class ConversationAudioMessagePlayer
311296
audioMediaPlayer.setDataSource(context, Uri.parse(result.decodedAssetPath.toString()))
312297
audioMediaPlayer.prepare()
313298

314-
getOrBuildWavesMask(
315-
conversationId = conversationId,
316-
messageId = messageId,
317-
assetPath = result.decodedAssetPath
318-
)
319-
320299
if (position != null) audioMediaPlayer.seekTo(position)
321300

322301
audioFocusHelper.request()
@@ -366,49 +345,13 @@ class ConversationAudioMessagePlayer
366345
seekToAudioPosition.emit(MessageIdWrapper(conversationId, messageId) to position)
367346
}
368347

369-
/**
370-
* Gets waves mask from local database for the audio message or builds a new one if it does not exist and emits the update event.
371-
* @param conversationId The ID of the conversation.
372-
* @param messageId The ID of the message.
373-
* @param assetPath Optional path to the decoded asset. If not provided, it will be fetched.
374-
*/
375-
@Suppress("ReturnCount")
376-
suspend fun getOrBuildWavesMask(conversationId: ConversationId, messageId: String, assetPath: Path? = null) {
348+
suspend fun preloadAudioMessage(conversationId: ConversationId, messageId: String) {
377349
val currentAccountResult = coreLogic.getGlobalScope().session.currentSession()
378350
if (currentAccountResult is CurrentSessionResult.Failure) return
379351
val userId = (currentAccountResult as CurrentSessionResult.Success).accountInfo.userId
380-
val messageResult = coreLogic.getSessionScope(userId).messages.getMessageById(conversationId, messageId)
381-
if (messageResult is GetMessageByIdUseCase.Result.Failure) return
382-
val messageContent = (messageResult as GetMessageByIdUseCase.Result.Success).message.content
383-
if (messageContent !is MessageContent.Asset) return
384-
if (messageContent.value.metadata !is AssetMetadata.Audio) return
385-
val audioMetadata = messageContent.value.metadata as AssetMetadata.Audio
386-
387-
val currentNormalizedLoudness: ByteArray? = audioMetadata.normalizedLoudness ?: run {
388-
(assetPath ?: getAssetPath(userId, conversationId, messageId))?.let { assetPath ->
389-
audioNormalizedLoudnessBuilder(assetPath.toString())?.also {
390-
coreLogic.getSessionScope(userId).messages.updateAudioMessageNormalizedLoudnessUseCase(
391-
conversationId = conversationId,
392-
messageId = messageId,
393-
normalizedLoudness = it
394-
)
395-
}
396-
}
397-
}
398-
currentNormalizedLoudness?.let {
399-
audioMessageStateUpdate.emit(
400-
AudioMediaPlayerStateUpdate.WaveMaskUpdate(
401-
conversationId = conversationId,
402-
messageId = messageId,
403-
waveMask = currentNormalizedLoudness.toWavesMask()
404-
)
405-
)
406-
}
352+
getAssetMessage(userId, conversationId, messageId)
407353
}
408354

409-
private suspend fun getAssetPath(userId: UserId, conversationId: ConversationId, messageId: String): Path? =
410-
(getAssetMessage(userId, conversationId, messageId) as? MessageAssetResult.Success)?.decodedAssetPath
411-
412355
private suspend fun resumeOrPause(conversationId: ConversationId, messageId: String) {
413356
if (audioMediaPlayer.isPlaying) {
414357
pause(conversationId, messageId)

app/src/main/kotlin/com/wire/android/media/audiomessage/RecordAudioMessagePlayer.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import android.content.Context
2121
import android.media.MediaPlayer
2222
import androidx.core.net.toUri
2323
import com.wire.android.di.ApplicationScope
24-
import com.wire.kalium.logic.feature.asset.AudioNormalizedLoudnessBuilder
2524
import dagger.hilt.android.scopes.ViewModelScoped
2625
import kotlinx.coroutines.CoroutineScope
2726
import kotlinx.coroutines.channels.BufferOverflow
@@ -41,7 +40,6 @@ import javax.inject.Inject
4140
class RecordAudioMessagePlayer @Inject constructor(
4241
private val context: Context,
4342
private val audioMediaPlayer: MediaPlayer,
44-
private val audioNormalizedLoudnessBuilder: AudioNormalizedLoudnessBuilder,
4543
private val audioFocusHelper: AudioFocusHelper,
4644
@ApplicationScope private val scope: CoroutineScope
4745
) {
@@ -122,12 +120,6 @@ class RecordAudioMessagePlayer @Inject constructor(
122120
)
123121
)
124122
}
125-
126-
is RecordAudioMediaPlayerStateUpdate.WaveMaskUpdate -> {
127-
audioState = audioState.copy(
128-
wavesMask = audioStateUpdate.waveMask
129-
)
130-
}
131123
}
132124

133125
audioState
@@ -185,14 +177,6 @@ class RecordAudioMessagePlayer @Inject constructor(
185177
audioMediaPlayer.seekTo(position)
186178
audioMediaPlayer.start()
187179

188-
audioNormalizedLoudnessBuilder(audioFile.path)?.let {
189-
audioMessageStateUpdate.emit(
190-
RecordAudioMediaPlayerStateUpdate.WaveMaskUpdate(
191-
waveMask = it.toWavesMask()
192-
)
193-
)
194-
}
195-
196180
audioMessageStateUpdate.emit(
197181
RecordAudioMediaPlayerStateUpdate.RecordAudioMediaPlayingStateUpdate(
198182
audioMediaPlayingState = AudioMediaPlayingState.Playing

app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageStyle.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,18 @@ fun MessageStyle.textAlign(): TextAlign {
112112
}
113113
}
114114

115-
private const val BUBBLE_OPACITY = 0.5F
115+
@Composable
116+
fun MessageStyle.playedColor() = when (this) {
117+
MessageStyle.BUBBLE_SELF -> colorsScheme().selfBubble.onPrimary
118+
MessageStyle.BUBBLE_OTHER -> colorsScheme().otherBubble.primaryOnSecondary
119+
MessageStyle.NORMAL -> colorsScheme().primary
120+
}
121+
122+
@Composable
123+
fun MessageStyle.remainingColor() = when (this) {
124+
MessageStyle.BUBBLE_SELF -> colorsScheme().selfBubble.onPrimary.copy(alpha = 0.5F)
125+
MessageStyle.BUBBLE_OTHER -> colorsScheme().secondaryText
126+
MessageStyle.NORMAL -> colorsScheme().onTertiaryButtonDisabled
127+
}
128+
129+
private const val BUBBLE_OPACITY = 0.7F

app/src/main/kotlin/com/wire/android/ui/home/conversations/mock/Mock.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,8 +350,12 @@ fun mockAssetMessage(assetId: String = "asset1", messageId: String = "msg1") = U
350350
source = MessageSource.Self
351351
)
352352

353-
fun mockAssetAudioMessage(assetId: String = "asset1", messageId: String = "msg1") = UIMessage.Regular(
354-
conversationId = ConversationId("value", "domain"),
353+
fun mockAssetAudioMessage(
354+
assetId: String = "asset1",
355+
messageId: String = "msg1",
356+
conversationId: ConversationId = ConversationId("value", "domain"),
357+
) = UIMessage.Regular(
358+
conversationId = conversationId,
355359
userAvatarData = UserAvatarData(
356360
UserAvatarAsset(UserAssetId("a", "domain")),
357361
UserAvailabilityStatus.AVAILABLE

0 commit comments

Comments
 (0)