From 763b60ee6b5dae8b8aa7c7b5e7ccb9654b38bdd5 Mon Sep 17 00:00:00 2001 From: Florian Renaud 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 = 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(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 {