diff --git a/changelog.d/7450.wip b/changelog.d/7450.wip new file mode 100644 index 00000000000..de4d3dc5e1c --- /dev/null +++ b/changelog.d/7450.wip @@ -0,0 +1 @@ +[Voice Broadcast] Stop recording when opening the room after an app restart diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 61a8e5b79ec..c3abdde022f 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -42,6 +42,7 @@ import im.vector.app.features.raw.wellknown.isSecureBackupRequired import im.vector.app.features.raw.wellknown.withElementWellKnown import im.vector.app.features.session.coroutineScope import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.voicebroadcast.usecase.StopOngoingVoiceBroadcastUseCase import im.vector.lib.core.utils.compat.getParcelableExtraCompat import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -92,6 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor( private val analyticsConfig: AnalyticsConfig, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, private val vectorFeatures: VectorFeatures, + private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase, ) : VectorViewModel(initialState) { @AssistedFactory @@ -123,6 +125,7 @@ class HomeActivityViewModel @AssistedInject constructor( observeReleaseNotes() observeLocalNotificationsSilenced() initThreadsMigration() + viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() } } private fun observeReleaseNotes() = withState { state -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt index 55ec922a578..e01dd31516f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerFragment.kt @@ -234,7 +234,7 @@ class MessageComposerFragment : VectorBaseFragment(), A } // TODO remove this when there will be a recording indicator outside of the timeline // Pause voice broadcast if the timeline is not shown anymore - it.isVoiceBroadcasting && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause) + it.isRecordingVoiceBroadcast && !requireActivity().isChangingConfigurations -> timelineViewModel.handle(VoiceBroadcastAction.Recording.Pause) else -> { timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause) messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString())) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt index 0df1dbebd86..a4021f87b21 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewState.kt @@ -79,9 +79,8 @@ data class MessageComposerViewState( is VoiceMessageRecorderView.RecordingUiState.Recording -> true } - val isVoiceBroadcasting = when (voiceBroadcastState) { + val isRecordingVoiceBroadcast = when (voiceBroadcastState) { VoiceBroadcastState.STARTED, - VoiceBroadcastState.PAUSED, VoiceBroadcastState.RESUMED -> true else -> false } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt new file mode 100644 index 00000000000..cb228ad8aaa --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetOngoingVoiceBroadcastsUseCase.kt @@ -0,0 +1,50 @@ +/* + * 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.core.di.ActiveSessionHolder +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +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 org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.getRoom +import timber.log.Timber +import javax.inject.Inject + +class GetOngoingVoiceBroadcastsUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, +) { + + fun execute(roomId: String): List { + println("## GetOngoingVoiceBroadcastsUseCase") + println("## GetOngoingVoiceBroadcastsUseCase activeSessionHolder $activeSessionHolder") + val session = activeSessionHolder.getSafeActiveSession() + println("## GetOngoingVoiceBroadcastsUseCase session $session") + val room = session?.getRoom(roomId) ?: error("Unknown roomId: $roomId") + println("## GetOngoingVoiceBroadcastsUseCase room $room") + + Timber.d("## GetLastVoiceBroadcastUseCase: get last voice broadcast in $roomId") + + return room.stateService().getStateEvents( + setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), + QueryStringValue.IsNotEmpty + ) + .mapNotNull { it.asVoiceBroadcastEvent() } + .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt index 7934d18e360..2b7ca7b9f16 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCase.kt @@ -25,9 +25,7 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState -import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.lib.multipicker.utils.toMultiPickerAudioType -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.events.model.toContent @@ -43,6 +41,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val context: Context, private val buildMeta: BuildMeta, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, ) { suspend fun execute(roomId: String): Result = runCatching { @@ -50,12 +49,7 @@ class StartVoiceBroadcastUseCase @Inject constructor( Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested") - val onGoingVoiceBroadcastEvents = room.stateService().getStateEvents( - setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), - QueryStringValue.IsNotEmpty - ) - .mapNotNull { it.asVoiceBroadcastEvent() } - .filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + val onGoingVoiceBroadcastEvents = getOngoingVoiceBroadcastsUseCase.execute(roomId) if (onGoingVoiceBroadcastEvents.isEmpty()) { startVoiceBroadcast(room) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt new file mode 100644 index 00000000000..ab4d16ab600 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/StopOngoingVoiceBroadcastUseCase.kt @@ -0,0 +1,64 @@ +/* + * 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.core.di.ActiveSessionHolder +import im.vector.app.features.voicebroadcast.VoiceBroadcastHelper +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams +import timber.log.Timber +import javax.inject.Inject + +/** + * Stop ongoing voice broadcast if any. + */ +class StopOngoingVoiceBroadcastUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder, + private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase, + private val voiceBroadcastHelper: VoiceBroadcastHelper, +) { + + suspend fun execute() { + Timber.d("## StopOngoingVoiceBroadcastUseCase: Stop ongoing voice broadcast requested") + + val session = activeSessionHolder.getSafeActiveSession() ?: run { + Timber.w("## StopOngoingVoiceBroadcastUseCase: no active session") + return + } + // FIXME Iterate only on recent rooms for the moment, improve this + val recentRooms = session.roomService() + .getBreadcrumbs(roomSummaryQueryParams { + displayName = QueryStringValue.NoCondition + memberships = listOf(Membership.JOIN) + }) + .mapNotNull { session.getRoom(it.roomId) } + + recentRooms + .forEach { room -> + val ongoingVoiceBroadcasts = getOngoingVoiceBroadcastsUseCase.execute(room.roomId) + val myOngoingVoiceBroadcastId = ongoingVoiceBroadcasts.find { it.root.stateKey == session.myUserId }?.reference?.eventId + val initialEvent = myOngoingVoiceBroadcastId?.let { room.timelineService().getTimelineEvent(it)?.root?.asVoiceBroadcastEvent() } + if (myOngoingVoiceBroadcastId != null && initialEvent?.content?.deviceId == session.sessionParams.deviceId) { + voiceBroadcastHelper.stopVoiceBroadcast(room.roomId) + return // No need to iterate more as we should not have more than one recording VB + } + } + } +} diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt index 9fa6b7a4503..217a395076b 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt @@ -20,6 +20,7 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder 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.test.fakes.FakeContext import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoomService @@ -27,13 +28,13 @@ import im.vector.app.test.fakes.FakeSession import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import io.mockk.slot import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBeNull import org.junit.Test -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -48,11 +49,13 @@ class StartVoiceBroadcastUseCaseTest { private val fakeRoom = FakeRoom() private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeVoiceBroadcastRecorder = mockk(relaxed = true) + private val fakeGetOngoingVoiceBroadcastsUseCase = mockk() private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase( - fakeSession, - fakeVoiceBroadcastRecorder, - FakeContext().instance, - mockk() + session = fakeSession, + voiceBroadcastRecorder = fakeVoiceBroadcastRecorder, + context = FakeContext().instance, + buildMeta = mockk(), + getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase, ) @Test @@ -80,7 +83,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List) { // Given clearAllMocks() - givenAVoiceBroadcasts(voiceBroadcasts) + givenVoiceBroadcasts(voiceBroadcasts) val voiceBroadcastInfoContentInterceptor = slot() coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID } @@ -103,7 +106,7 @@ class StartVoiceBroadcastUseCaseTest { private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List) { // Given clearAllMocks() - givenAVoiceBroadcasts(voiceBroadcasts) + givenVoiceBroadcasts(voiceBroadcasts) // When startVoiceBroadcastUseCase.execute(A_ROOM_ID) @@ -112,7 +115,7 @@ class StartVoiceBroadcastUseCaseTest { coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) } } - private fun givenAVoiceBroadcasts(voiceBroadcasts: List) { + private fun givenVoiceBroadcasts(voiceBroadcasts: List) { val events = voiceBroadcasts.map { Event( type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, @@ -122,7 +125,9 @@ class StartVoiceBroadcastUseCaseTest { ).toContent() ) } - fakeRoom.stateService().givenGetStateEvents(QueryStringValue.IsNotEmpty, events) + .mapNotNull { it.asVoiceBroadcastEvent() } + .filter { it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED } + every { fakeGetOngoingVoiceBroadcastsUseCase.execute(any()) } returns events } private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState)