diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 5b0e5b2b1c..79d59064e9 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -30,7 +30,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Stat import im.vector.app.features.voicebroadcast.listening.usecase.GetLiveVoiceBroadcastChunksUseCase import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase +import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn @@ -48,7 +48,7 @@ import javax.inject.Singleton class VoiceBroadcastPlayerImpl @Inject constructor( private val sessionHolder: ActiveSessionHolder, private val playbackTracker: AudioMessagePlaybackTracker, - private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase, + private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, private val getLiveVoiceBroadcastChunksUseCase: GetLiveVoiceBroadcastChunksUseCase ) : VoiceBroadcastPlayer { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt index 16b15b9a77..03e713eeaa 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt @@ -24,7 +24,7 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.sequence -import im.vector.app.features.voicebroadcast.usecase.GetVoiceBroadcastEventUseCase +import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase import im.vector.app.features.voicebroadcast.voiceBroadcastId import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -48,7 +48,7 @@ import javax.inject.Inject */ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, - private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastEventUseCase, + private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, ) { fun execute(voiceBroadcast: VoiceBroadcast): Flow> { diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt new file mode 100644 index 0000000000..b882d1625b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.voicebroadcast.usecase + +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.voiceBroadcastId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.transformWhile +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.mapOptional +import timber.log.Timber +import javax.inject.Inject + +class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor( + private val session: Session, +) { + + fun execute(voiceBroadcast: VoiceBroadcast): Flow> { + val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}") + return getMostRecentVoiceBroadcastEventFlow(room, voiceBroadcast) + .onEach { event -> + Timber.d( + "## VoiceBroadcast | " + + "voiceBroadcastId=${event.getOrNull()?.voiceBroadcastId}, " + + "state=${event.getOrNull()?.content?.voiceBroadcastState}" + ) + } + } + + /** + * Get a flow of the most recent event for the given voice broadcast. + */ + private fun getMostRecentVoiceBroadcastEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow> { + val startedEventFlow = room.flow().liveTimelineEvent(voiceBroadcast.voiceBroadcastId) + // observe started event changes + return startedEventFlow + .mapOptional { it.root.asVoiceBroadcastEvent() } + .flatMapLatest { startedEvent -> + if (startedEvent.hasValue().not() || startedEvent.get().root.isRedacted()) { + // if started event is null or redacted, send null + flowOf(Optional.empty()) + } else { + // otherwise, observe most recent event changes + getMostRecentRelatedEventFlow(room, voiceBroadcast) + .transformWhile { mostRecentEvent -> + emit(mostRecentEvent) + mostRecentEvent.hasValue() + } + .map { + if (!it.hasValue()) { + // no most recent event, fallback to started event + startedEvent + } else { + // otherwise, keep the most recent event + it + } + } + } + } + .distinctUntilChangedBy { it.getOrNull()?.content?.voiceBroadcastState } + } + + /** + * Get a flow of the most recent related event. + */ + private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow> { + val mostRecentEvent = getMostRecentRelatedEvent(room, voiceBroadcast).toOptional() + return if (mostRecentEvent.hasValue()) { + val stateKey = mostRecentEvent.get().root.stateKey.orEmpty() + // observe incoming voice broadcast state events + room.flow() + .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(stateKey)) + .mapOptional { it.asVoiceBroadcastEvent() } + // drop first event sent by the matrix-sdk, we compute manually this first event + .drop(1) + // start with the computed most recent event + .onStart { emit(mostRecentEvent) } + // handle event if null or related to the given voice broadcast + .filter { it.hasValue().not() || it.get().voiceBroadcastId == voiceBroadcast.voiceBroadcastId } + // observe changes while event is not null + .transformWhile { event -> + emit(event) + event.hasValue() + } + .flatMapLatest { newMostRecentEvent -> + if (newMostRecentEvent.hasValue()) { + // observe most recent event changes + newMostRecentEvent.get().flow() + .transformWhile { event -> + // observe changes until event is null or redacted + emit(event) + event.hasValue() && event.get().root.isRedacted().not() + } + .flatMapLatest { event -> + if (event.getOrNull()?.root?.isRedacted().orFalse()) { + // event is null or redacted, switch to the latest not redacted event + getMostRecentRelatedEventFlow(room, voiceBroadcast) + } else { + // event is not redacted, send the event + flowOf(event) + } + } + } else { + // there is no more most recent event, just send it + flowOf(newMostRecentEvent) + } + } + } else { + // there is no more most recent event, just send it + flowOf(mostRecentEvent) + } + } + + /** + * Get the most recent event related to the given voice broadcast. + */ + private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { + return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) + .mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeUnless { it.root.isRedacted() } } + .maxByOrNull { it.root.originServerTs ?: 0 } + } + + /** + * Get a flow of the given voice broadcast event changes. + */ + private fun VoiceBroadcastEvent.flow(): Flow> { + val room = this.root.roomId?.let { session.getRoom(it) } ?: return flowOf(Optional.empty()) + return room.flow().liveTimelineEvent(root.eventId!!).mapOptional { it.root.asVoiceBroadcastEvent() } + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt deleted file mode 100644 index 94eca2b54e..0000000000 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.voicebroadcast.usecase - -import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants -import im.vector.app.features.voicebroadcast.model.VoiceBroadcast -import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.voiceBroadcastId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.onStart -import org.matrix.android.sdk.api.query.QueryStringValue -import org.matrix.android.sdk.api.session.Session -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.util.Optional -import org.matrix.android.sdk.api.util.toOptional -import org.matrix.android.sdk.flow.flow -import org.matrix.android.sdk.flow.mapOptional -import timber.log.Timber -import javax.inject.Inject - -class GetVoiceBroadcastEventUseCase @Inject constructor( - private val session: Session, -) { - - fun execute(voiceBroadcast: VoiceBroadcast): Flow> { - val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}") - - Timber.d("## GetVoiceBroadcastUseCase: get voice broadcast $voiceBroadcast") - - val initialEvent = room.timelineService().getTimelineEvent(voiceBroadcast.voiceBroadcastId)?.root?.asVoiceBroadcastEvent() - val latestEvent = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) - .mapNotNull { it.root.asVoiceBroadcastEvent() } - .maxByOrNull { it.root.originServerTs ?: 0 } - ?: initialEvent - - return when (latestEvent?.content?.voiceBroadcastState) { - null, VoiceBroadcastState.STOPPED -> flowOf(latestEvent.toOptional()) - else -> { - room.flow() - .liveStateEvent(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, QueryStringValue.Equals(latestEvent.root.stateKey.orEmpty())) - .onStart { emit(latestEvent.root.toOptional()) } - .distinctUntilChanged() - .filter { !it.hasValue() || it.getOrNull()?.asVoiceBroadcastEvent()?.voiceBroadcastId == voiceBroadcast.voiceBroadcastId } - .mapOptional { it.asVoiceBroadcastEvent() } - } - } - } -}