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)
     }