From 2d60e49205b28a6d8199060afdf9089eceed72e9 Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Wed, 23 Nov 2022 12:09:41 +0100 Subject: [PATCH 01/12] Handle redaction when observing voice broadcast state changes --- .../listening/VoiceBroadcastPlayerImpl.kt | 4 +- .../GetLiveVoiceBroadcastChunksUseCase.kt | 4 +- ...stRecentVoiceBroadcastStateEventUseCase.kt | 163 ++++++++++++++++++ .../usecase/GetVoiceBroadcastEventUseCase.kt | 68 -------- 4 files changed, 167 insertions(+), 72 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt delete mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastEventUseCase.kt 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<List<MessageAudioEvent>> { 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<Optional<VoiceBroadcastEvent>> { + 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<Optional<VoiceBroadcastEvent>> { + 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<Optional<VoiceBroadcastEvent>> { + 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<Optional<VoiceBroadcastEvent>> { + 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<Optional<VoiceBroadcastEvent>> { - 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() } - } - } - } -} From f436de12300b766fadfaf2c93300ac8bf9e01614 Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Wed, 23 Nov 2022 14:10:44 +0100 Subject: [PATCH 02/12] Handle voice broadcast deletion on listener side --- .../listening/VoiceBroadcastPlayerImpl.kt | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) 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 79d59064e9..f04b85859b 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 @@ -145,19 +145,25 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playingState = State.BUFFERING - observeVoiceBroadcastLiveState(voiceBroadcast) + observeVoiceBroadcastStateEvent(voiceBroadcast) fetchPlaylistAndStartPlayback(voiceBroadcast) } - private fun observeVoiceBroadcastLiveState(voiceBroadcast: VoiceBroadcast) { + private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) { voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) - .onEach { - currentVoiceBroadcastEvent = it.getOrNull() - updateLiveListeningMode() - } + .onEach { onVoiceBroadcastStateEventUpdated(it.getOrNull()) } .launchIn(sessionScope) } + private fun onVoiceBroadcastStateEventUpdated(event: VoiceBroadcastEvent?) { + if (event == null) { + stop() + } else { + currentVoiceBroadcastEvent = event + updateLiveListeningMode() + } + } + private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) .onEach { From 763b60ee6b5dae8b8aa7c7b5e7ccb9654b38bdd5 Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Wed, 23 Nov 2022 17:31:29 +0100 Subject: [PATCH 03/12] Update voice broadcast recorder according to the most recent voice broadcast state event --- .../java/im/vector/app/core/di/VoiceModule.kt | 13 ++++- .../recording/VoiceBroadcastRecorder.kt | 3 +- .../recording/VoiceBroadcastRecorderQ.kt | 58 ++++++++++++++++--- .../usecase/PauseVoiceBroadcastUseCase.kt | 6 -- .../usecase/ResumeVoiceBroadcastUseCase.kt | 10 +--- .../usecase/StartVoiceBroadcastUseCase.kt | 19 ++++-- .../ResumeVoiceBroadcastUseCaseTest.kt | 5 +- 7 files changed, 78 insertions(+), 36 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt index 30a8565771..6437326294 100644 --- a/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/VoiceModule.kt @@ -27,6 +27,7 @@ import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayerImpl import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorderQ +import im.vector.app.features.voicebroadcast.usecase.GetMostRecentVoiceBroadcastStateEventUseCase import javax.inject.Singleton @InstallIn(SingletonComponent::class) @@ -36,9 +37,17 @@ abstract class VoiceModule { companion object { @Provides @Singleton - fun providesVoiceBroadcastRecorder(context: Context): VoiceBroadcastRecorder? { + fun providesVoiceBroadcastRecorder( + context: Context, + sessionHolder: ActiveSessionHolder, + getMostRecentVoiceBroadcastStateEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase, + ): VoiceBroadcastRecorder? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - VoiceBroadcastRecorderQ(context) + VoiceBroadcastRecorderQ( + context = context, + sessionHolder = sessionHolder, + getVoiceBroadcastEventUseCase = getMostRecentVoiceBroadcastStateEventUseCase + ) } else { null } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt index bc13d1fea8..00e4bb17dd 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorder.kt @@ -18,6 +18,7 @@ package im.vector.app.features.voicebroadcast.recording import androidx.annotation.IntRange import im.vector.app.features.voice.VoiceRecorder +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import java.io.File interface VoiceBroadcastRecorder : VoiceRecorder { @@ -31,7 +32,7 @@ interface VoiceBroadcastRecorder : VoiceRecorder { /** Current remaining time of recording, in seconds, if any. */ val currentRemainingTime: Long? - fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) + fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int) fun addListener(listener: Listener) fun removeListener(listener: Listener) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index c5408b768b..483b88f57c 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -20,8 +20,17 @@ import android.content.Context import android.media.MediaRecorder import android.os.Build import androidx.annotation.RequiresApi +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.session.coroutineScope import im.vector.app.features.voice.AbstractVoiceRecorderQ +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.usecase.GetMostRecentVoiceBroadcastStateEventUseCase import im.vector.lib.core.utils.timer.CountUpTimer +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.content.ContentAttachmentData import java.util.concurrent.CopyOnWriteArrayList @@ -30,10 +39,17 @@ import java.util.concurrent.TimeUnit @RequiresApi(Build.VERSION_CODES.Q) class VoiceBroadcastRecorderQ( context: Context, + private val sessionHolder: ActiveSessionHolder, + private val getVoiceBroadcastEventUseCase: GetMostRecentVoiceBroadcastStateEventUseCase ) : AbstractVoiceRecorderQ(context), VoiceBroadcastRecorder { + private val session get() = sessionHolder.getActiveSession() + private val sessionScope get() = session.coroutineScope + + private var voiceBroadcastStateObserver: Job? = null + private var maxFileSize = 0L // zero or negative for no limit - private var currentRoomId: String? = null + private var currentVoiceBroadcast: VoiceBroadcast? = null private var currentMaxLength: Int = 0 override var currentSequence = 0 @@ -68,14 +84,16 @@ class VoiceBroadcastRecorderQ( } } - override fun startRecord(roomId: String, chunkLength: Int, maxLength: Int) { - currentRoomId = roomId + override fun startRecordVoiceBroadcast(voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int) { + // Stop recording previous voice broadcast if any + if (recordingState != VoiceBroadcastRecorder.State.Idle) stopRecord() + + currentVoiceBroadcast = voiceBroadcast maxFileSize = (chunkLength * audioEncodingBitRate / 8).toLong() currentMaxLength = maxLength currentSequence = 1 - startRecord(roomId) - recordingState = VoiceBroadcastRecorder.State.Recording - recordingTicker.start() + + observeVoiceBroadcastStateEvent(voiceBroadcast) } override fun pauseRecord() { @@ -88,7 +106,7 @@ class VoiceBroadcastRecorderQ( override fun resumeRecord() { currentSequence++ - currentRoomId?.let { startRecord(it) } + currentVoiceBroadcast?.let { startRecord(it.roomId) } recordingState = VoiceBroadcastRecorder.State.Recording recordingTicker.resume() } @@ -104,11 +122,15 @@ class VoiceBroadcastRecorderQ( // Remove listeners listeners.clear() + // Do not observe anymore voice broadcast changes + voiceBroadcastStateObserver?.cancel() + voiceBroadcastStateObserver = null + // Reset data currentSequence = 0 currentMaxLength = 0 currentRemainingTime = null - currentRoomId = null + currentVoiceBroadcast = null } override fun release() { @@ -126,6 +148,26 @@ class VoiceBroadcastRecorderQ( listeners.remove(listener) } + private fun observeVoiceBroadcastStateEvent(voiceBroadcast: VoiceBroadcast) { + voiceBroadcastStateObserver = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) + .onEach { onVoiceBroadcastStateEventUpdated(voiceBroadcast, it.getOrNull()) } + .launchIn(sessionScope) + } + + private fun onVoiceBroadcastStateEventUpdated(voiceBroadcast: VoiceBroadcast, event: VoiceBroadcastEvent?) { + when (event?.content?.voiceBroadcastState) { + VoiceBroadcastState.STARTED -> { + startRecord(voiceBroadcast.roomId) + recordingState = VoiceBroadcastRecorder.State.Recording + recordingTicker.start() + } + VoiceBroadcastState.PAUSED -> pauseRecord() + VoiceBroadcastState.RESUMED -> resumeRecord() + VoiceBroadcastState.STOPPED, + null -> stopRecord() + } + } + private fun onMaxFileSizeApproaching(roomId: String) { setNextOutputFile(roomId) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt index 58e1f26f44..3ce6e4a533 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt @@ -62,11 +62,5 @@ class PauseVoiceBroadcastUseCase @Inject constructor( lastChunkSequence = voiceBroadcastRecorder?.currentSequence, ).toContent(), ) - - pauseRecording() - } - - private fun pauseRecording() { - voiceBroadcastRecorder?.pauseRecord() } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt index 524b64e095..5ad5b0704d 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt @@ -20,7 +20,6 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder 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.toContent @@ -31,8 +30,7 @@ import timber.log.Timber import javax.inject.Inject class ResumeVoiceBroadcastUseCase @Inject constructor( - private val session: Session, - private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, + private val session: Session ) { suspend fun execute(roomId: String): Result<Unit> = runCatching { @@ -66,11 +64,5 @@ class ResumeVoiceBroadcastUseCase @Inject constructor( voiceBroadcastStateStr = VoiceBroadcastState.RESUMED.value, ).toContent(), ) - - resumeRecording() - } - - private fun resumeRecording() { - voiceBroadcastRecorder?.resumeRecord() } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 45f622ad92..20f0615863 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -24,11 +24,13 @@ import im.vector.app.features.session.coroutineScope import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase import im.vector.lib.multipicker.utils.toMultiPickerAudioType +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting import org.matrix.android.sdk.api.query.QueryStringValue @@ -43,6 +45,8 @@ import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.flow.flow +import org.matrix.android.sdk.flow.unwrap import timber.log.Timber import java.io.File import javax.inject.Inject @@ -63,6 +67,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( assertCanStartVoiceBroadcast(room) startVoiceBroadcast(room) + return Result.success(Unit) } private suspend fun startVoiceBroadcast(room: Room) { @@ -79,13 +84,15 @@ class StartVoiceBroadcastUseCase @Inject constructor( ).toContent() ) - startRecording(room, eventId, chunkLength, maxLength) + val voiceBroadcast = VoiceBroadcast(roomId = room.roomId, voiceBroadcastId = eventId) + room.flow().liveTimelineEvent(eventId).unwrap().first() // wait for the event come back from the sync + startRecording(room, voiceBroadcast, chunkLength, maxLength) } - private fun startRecording(room: Room, eventId: String, chunkLength: Int, maxLength: Int) { + private fun startRecording(room: Room, voiceBroadcast: VoiceBroadcast, chunkLength: Int, maxLength: Int) { voiceBroadcastRecorder?.addListener(object : VoiceBroadcastRecorder.Listener { override fun onVoiceMessageCreated(file: File, sequence: Int) { - sendVoiceFile(room, file, eventId, sequence) + sendVoiceFile(room, file, voiceBroadcast, sequence) } override fun onRemainingTimeUpdated(remainingTime: Long?) { @@ -94,10 +101,10 @@ class StartVoiceBroadcastUseCase @Inject constructor( } } }) - voiceBroadcastRecorder?.startRecord(room.roomId, chunkLength, maxLength) + voiceBroadcastRecorder?.startRecordVoiceBroadcast(voiceBroadcast, chunkLength, maxLength) } - private fun sendVoiceFile(room: Room, voiceMessageFile: File, referenceEventId: String, sequence: Int) { + private fun sendVoiceFile(room: Room, voiceMessageFile: File, voiceBroadcast: VoiceBroadcast, sequence: Int) { val outputFileUri = FileProvider.getUriForFile( context, buildMeta.applicationId + ".fileProvider", @@ -109,7 +116,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( attachment = audioType.toContentAttachmentData(isVoiceMessage = true), compressBeforeSending = false, roomIds = emptySet(), - relatesTo = RelationDefaultContent(RelationType.REFERENCE, referenceEventId), + relatesTo = RelationDefaultContent(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId), additionalContent = mapOf( VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY to VoiceBroadcastChunk(sequence = sequence).toContent() ) diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt index 8b66d45dd4..7fe74052a9 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/ResumeVoiceBroadcastUseCaseTest.kt @@ -19,7 +19,6 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.recording.usecase.ResumeVoiceBroadcastUseCase import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService @@ -27,7 +26,6 @@ import im.vector.app.test.fakes.FakeSession import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify -import io.mockk.mockk import io.mockk.slot import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe @@ -47,8 +45,7 @@ class ResumeVoiceBroadcastUseCaseTest { private val fakeRoom = FakeRoom() private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) - private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true) - private val resumeVoiceBroadcastUseCase = ResumeVoiceBroadcastUseCase(fakeSession, fakeVoiceBroadcastRecorder) + private val resumeVoiceBroadcastUseCase = ResumeVoiceBroadcastUseCase(fakeSession) @Test fun `given a room id with a potential existing voice broadcast state when calling execute then the voice broadcast is resumed or not`() = runTest { From 3ebcd8c1f4274f86c2d55052e61941be80978b3e Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Wed, 23 Nov 2022 17:36:59 +0100 Subject: [PATCH 04/12] changelog --- changelog.d/7629.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7629.wip diff --git a/changelog.d/7629.wip b/changelog.d/7629.wip new file mode 100644 index 0000000000..ecc4449b6f --- /dev/null +++ b/changelog.d/7629.wip @@ -0,0 +1 @@ +Voice Broadcast - Handle redaction of the state events on the listener and recorder sides From 023326a20db38885bad98449b3c4e345528b968f Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Thu, 24 Nov 2022 10:18:58 +0100 Subject: [PATCH 05/12] Do not wait for state event feedback for pause/stop actions on the recorder --- .../voicebroadcast/recording/VoiceBroadcastRecorderQ.kt | 2 ++ .../recording/usecase/PauseVoiceBroadcastUseCase.kt | 8 ++++++++ .../recording/usecase/StopVoiceBroadcastUseCase.kt | 6 ++++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt index 483b88f57c..b751417ca6 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/VoiceBroadcastRecorderQ.kt @@ -97,6 +97,7 @@ class VoiceBroadcastRecorderQ( } override fun pauseRecord() { + if (recordingState != VoiceBroadcastRecorder.State.Recording) return tryOrNull { mediaRecorder?.stop() } mediaRecorder?.reset() recordingState = VoiceBroadcastRecorder.State.Paused @@ -105,6 +106,7 @@ class VoiceBroadcastRecorderQ( } override fun resumeRecord() { + if (recordingState != VoiceBroadcastRecorder.State.Paused) return currentSequence++ currentVoiceBroadcast?.let { startRecord(it.roomId) } recordingState = VoiceBroadcastRecorder.State.Recording diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt index 3ce6e4a533..817c1a72e4 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt @@ -53,6 +53,10 @@ class PauseVoiceBroadcastUseCase @Inject constructor( private suspend fun pauseVoiceBroadcast(room: Room, reference: RelationDefaultContent?) { Timber.d("## PauseVoiceBroadcastUseCase: Send new voice broadcast info state event") + + // immediately pause the recording + pauseRecording() + room.stateService().sendStateEvent( eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, stateKey = session.myUserId, @@ -63,4 +67,8 @@ class PauseVoiceBroadcastUseCase @Inject constructor( ).toContent(), ) } + + private fun pauseRecording() { + voiceBroadcastRecorder?.pauseRecord() + } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt index da13100609..cd70671e76 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt @@ -54,6 +54,10 @@ class StopVoiceBroadcastUseCase @Inject constructor( private suspend fun stopVoiceBroadcast(room: Room, reference: RelationDefaultContent?) { Timber.d("## StopVoiceBroadcastUseCase: Send new voice broadcast info state event") + + // Immediately stop the recording + stopRecording() + room.stateService().sendStateEvent( eventType = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, stateKey = session.myUserId, @@ -63,8 +67,6 @@ class StopVoiceBroadcastUseCase @Inject constructor( lastChunkSequence = voiceBroadcastRecorder?.currentSequence, ).toContent(), ) - - stopRecording() } private fun stopRecording() { From a2dee2193afa8d451cff9b012ae33ad40bd0f2cc Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Thu, 24 Nov 2022 16:36:35 +0100 Subject: [PATCH 06/12] Fix bad condition --- .../usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index b882d1625b..a401e8c157 100644 --- 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 @@ -31,7 +31,7 @@ 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.extensions.orTrue 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 @@ -125,7 +125,7 @@ class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor( event.hasValue() && event.get().root.isRedacted().not() } .flatMapLatest { event -> - if (event.getOrNull()?.root?.isRedacted().orFalse()) { + if (event.getOrNull()?.root?.isRedacted().orTrue()) { // event is null or redacted, switch to the latest not redacted event getMostRecentRelatedEventFlow(room, voiceBroadcast) } else { From d092c837745b48d588d38036fe4ff1d12368d2fa Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Thu, 24 Nov 2022 17:49:16 +0100 Subject: [PATCH 07/12] Fix wrong sequence number in stopped state event content --- .../recording/usecase/PauseVoiceBroadcastUseCase.kt | 5 +++-- .../recording/usecase/StopVoiceBroadcastUseCase.kt | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt index 817c1a72e4..0b22d7adf5 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/PauseVoiceBroadcastUseCase.kt @@ -54,7 +54,8 @@ class PauseVoiceBroadcastUseCase @Inject constructor( private suspend fun pauseVoiceBroadcast(room: Room, reference: RelationDefaultContent?) { Timber.d("## PauseVoiceBroadcastUseCase: Send new voice broadcast info state event") - // immediately pause the recording + // save the last sequence number and immediately pause the recording + val lastSequence = voiceBroadcastRecorder?.currentSequence pauseRecording() room.stateService().sendStateEvent( @@ -63,7 +64,7 @@ class PauseVoiceBroadcastUseCase @Inject constructor( body = MessageVoiceBroadcastInfoContent( relatesTo = reference, voiceBroadcastStateStr = VoiceBroadcastState.PAUSED.value, - lastChunkSequence = voiceBroadcastRecorder?.currentSequence, + lastChunkSequence = lastSequence, ).toContent(), ) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt index cd70671e76..b93bd346db 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StopVoiceBroadcastUseCase.kt @@ -55,7 +55,8 @@ class StopVoiceBroadcastUseCase @Inject constructor( private suspend fun stopVoiceBroadcast(room: Room, reference: RelationDefaultContent?) { Timber.d("## StopVoiceBroadcastUseCase: Send new voice broadcast info state event") - // Immediately stop the recording + // save the last sequence number and immediately stop the recording + val lastSequence = voiceBroadcastRecorder?.currentSequence stopRecording() room.stateService().sendStateEvent( @@ -64,7 +65,7 @@ class StopVoiceBroadcastUseCase @Inject constructor( body = MessageVoiceBroadcastInfoContent( relatesTo = reference, voiceBroadcastStateStr = VoiceBroadcastState.STOPPED.value, - lastChunkSequence = voiceBroadcastRecorder?.currentSequence, + lastChunkSequence = lastSequence, ).toContent(), ) } From 9dba6d7c8c4e3497b3ef146d67a1edbae2c64788 Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Thu, 24 Nov 2022 17:24:36 +0100 Subject: [PATCH 08/12] Fix issue on live playback detection --- .../listening/VoiceBroadcastPlayerImpl.kt | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) 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 f04b85859b..bd541d23e4 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 @@ -66,7 +66,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private var nextMediaPlayer: MediaPlayer? = null private var isPreparingNextPlayer: Boolean = false - private var currentVoiceBroadcastEvent: VoiceBroadcastEvent? = null + private var mostRecentVoiceBroadcastEvent: VoiceBroadcastEvent? = null override var currentVoiceBroadcast: VoiceBroadcast? = null override var isLiveListening: Boolean = false @@ -121,7 +121,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // Clear playlist playlist.reset() - currentVoiceBroadcastEvent = null + mostRecentVoiceBroadcastEvent = null currentVoiceBroadcast = null } @@ -159,7 +159,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (event == null) { stop() } else { - currentVoiceBroadcastEvent = event + mostRecentVoiceBroadcastEvent = event updateLiveListeningMode() } } @@ -204,7 +204,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val playlistItem = when { position != null -> playlist.findByPosition(position) - currentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull() + mostRecentVoiceBroadcastEvent?.isLive.orFalse() -> playlist.lastOrNull() else -> playlist.firstOrNull() } val content = playlistItem?.audioEvent?.content ?: run { Timber.w("## VoiceBroadcastPlayer: No content to play"); return } @@ -346,7 +346,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun updateLiveListeningMode(seekPosition: Int? = null) { isLiveListening = when { // the current voice broadcast is not live (ended) - currentVoiceBroadcastEvent?.isLive?.not().orFalse() -> false + mostRecentVoiceBroadcastEvent?.isLive != true -> false // the player is stopped or paused playingState == State.IDLE || playingState == State.PAUSED -> false seekPosition != null -> { @@ -412,13 +412,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onCompletion(mp: MediaPlayer) { if (nextMediaPlayer != null) return - val content = currentVoiceBroadcastEvent?.content - val isLive = content?.isLive.orFalse() - if (!isLive && content?.lastChunkSequence == playlist.currentSequence) { + if (isLiveListening || mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence == playlist.currentSequence) { + playingState = State.BUFFERING + } else { // We'll not receive new chunks anymore so we can stop the live listening stop() - } else { - playingState = State.BUFFERING } } From 4427156f0b734649a9d253f7f5d37d81cc250e9d Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Fri, 25 Nov 2022 15:27:46 +0100 Subject: [PATCH 09/12] Restore trailing comma --- .../recording/usecase/ResumeVoiceBroadcastUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt index 5ad5b0704d..5be726c03e 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/ResumeVoiceBroadcastUseCase.kt @@ -30,7 +30,7 @@ import timber.log.Timber import javax.inject.Inject class ResumeVoiceBroadcastUseCase @Inject constructor( - private val session: Session + private val session: Session, ) { suspend fun execute(roomId: String): Result<Unit> = runCatching { From aa53105f1713b237b06c059329175cc400784d5e Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Fri, 25 Nov 2022 15:32:29 +0100 Subject: [PATCH 10/12] improve flow stream --- ...MostRecentVoiceBroadcastStateEventUseCase.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) 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 index a401e8c157..1b3ba6ba22 100644 --- 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 @@ -27,7 +27,6 @@ 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 @@ -76,17 +75,15 @@ class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor( // 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 + val hasValue = mostRecentEvent.hasValue() + if (hasValue) { + // keep the most recent event + emit(mostRecentEvent) } else { - // otherwise, keep the most recent event - it + // no most recent event, fallback to started event + emit(startedEvent) } + hasValue } } } From 620bebc3a3f92f0f2bb0eadcd73c5468222a7f80 Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Fri, 25 Nov 2022 15:35:52 +0100 Subject: [PATCH 11/12] Rewrite condition for better clarity --- .../usecase/GetMostRecentVoiceBroadcastStateEventUseCase.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 1b3ba6ba22..e0179e403f 100644 --- 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 @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformWhile -import org.matrix.android.sdk.api.extensions.orTrue 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 @@ -122,7 +121,8 @@ class GetMostRecentVoiceBroadcastStateEventUseCase @Inject constructor( event.hasValue() && event.get().root.isRedacted().not() } .flatMapLatest { event -> - if (event.getOrNull()?.root?.isRedacted().orTrue()) { + val isRedactedOrNull = !event.hasValue() || event.get().root.isRedacted() + if (isRedactedOrNull) { // event is null or redacted, switch to the latest not redacted event getMostRecentRelatedEventFlow(room, voiceBroadcast) } else { From 9840731778fe9947f8b0d7f8612e07448bd621b1 Mon Sep 17 00:00:00 2001 From: Florian Renaud <florianr@element.io> Date: Mon, 28 Nov 2022 16:15:07 +0100 Subject: [PATCH 12/12] Add todo for missing unit test --- .../recording/usecase/StartVoiceBroadcastUseCase.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt index 20f0615863..e3814608ea 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt @@ -85,7 +85,10 @@ class StartVoiceBroadcastUseCase @Inject constructor( ) val voiceBroadcast = VoiceBroadcast(roomId = room.roomId, voiceBroadcastId = eventId) + + // TODO Update unit test to cover the following line room.flow().liveTimelineEvent(eventId).unwrap().first() // wait for the event come back from the sync + startRecording(room, voiceBroadcast, chunkLength, maxLength) }