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] 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 {