Merge pull request #7450 from vector-im/feature/fre/voice_broadcast_stop_on_app_restart

Voice Broadcast - Stop recording on app restart
This commit is contained in:
Florian Renaud 2022-10-26 15:49:32 +02:00 committed by GitHub
commit 6ee77ad101
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 136 additions and 20 deletions

1
changelog.d/7450.wip Normal file
View File

@ -0,0 +1 @@
[Voice Broadcast] Stop recording when opening the room after an app restart

View File

@ -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.raw.wellknown.withElementWellKnown
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voicebroadcast.usecase.StopOngoingVoiceBroadcastUseCase
import im.vector.lib.core.utils.compat.getParcelableExtraCompat import im.vector.lib.core.utils.compat.getParcelableExtraCompat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -92,6 +93,7 @@ class HomeActivityViewModel @AssistedInject constructor(
private val analyticsConfig: AnalyticsConfig, private val analyticsConfig: AnalyticsConfig,
private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore, private val releaseNotesPreferencesStore: ReleaseNotesPreferencesStore,
private val vectorFeatures: VectorFeatures, private val vectorFeatures: VectorFeatures,
private val stopOngoingVoiceBroadcastUseCase: StopOngoingVoiceBroadcastUseCase,
) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) { ) : VectorViewModel<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {
@AssistedFactory @AssistedFactory
@ -123,6 +125,7 @@ class HomeActivityViewModel @AssistedInject constructor(
observeReleaseNotes() observeReleaseNotes()
observeLocalNotificationsSilenced() observeLocalNotificationsSilenced()
initThreadsMigration() initThreadsMigration()
viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
} }
private fun observeReleaseNotes() = withState { state -> private fun observeReleaseNotes() = withState { state ->

View File

@ -242,7 +242,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), A
} }
// TODO remove this when there will be a recording indicator outside of the timeline // TODO remove this when there will be a recording indicator outside of the timeline
// Pause voice broadcast if the timeline is not shown anymore // 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 -> { else -> {
timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause) timelineViewModel.handle(VoiceBroadcastAction.Listening.Pause)
messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString())) messageComposerViewModel.handle(MessageComposerAction.OnEntersBackground(composer.text.toString()))

View File

@ -80,9 +80,8 @@ data class MessageComposerViewState(
is VoiceMessageRecorderView.RecordingUiState.Recording -> true is VoiceMessageRecorderView.RecordingUiState.Recording -> true
} }
val isVoiceBroadcasting = when (voiceBroadcastState) { val isRecordingVoiceBroadcast = when (voiceBroadcastState) {
VoiceBroadcastState.STARTED, VoiceBroadcastState.STARTED,
VoiceBroadcastState.PAUSED,
VoiceBroadcastState.RESUMED -> true VoiceBroadcastState.RESUMED -> true
else -> false else -> false
} }

View File

@ -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<VoiceBroadcastEvent> {
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 }
}
}

View File

@ -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.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import im.vector.lib.multipicker.utils.toMultiPickerAudioType 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.Session
import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
@ -43,6 +41,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val voiceBroadcastRecorder: VoiceBroadcastRecorder?,
private val context: Context, private val context: Context,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val getOngoingVoiceBroadcastsUseCase: GetOngoingVoiceBroadcastsUseCase,
) { ) {
suspend fun execute(roomId: String): Result<Unit> = runCatching { suspend fun execute(roomId: String): Result<Unit> = runCatching {
@ -50,12 +49,7 @@ class StartVoiceBroadcastUseCase @Inject constructor(
Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested") Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested")
val onGoingVoiceBroadcastEvents = room.stateService().getStateEvents( val onGoingVoiceBroadcastEvents = getOngoingVoiceBroadcastsUseCase.execute(roomId)
setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
QueryStringValue.IsNotEmpty
)
.mapNotNull { it.asVoiceBroadcastEvent() }
.filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
if (onGoingVoiceBroadcastEvents.isEmpty()) { if (onGoingVoiceBroadcastEvents.isEmpty()) {
startVoiceBroadcast(room) startVoiceBroadcast(room)

View File

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

View File

@ -20,6 +20,7 @@ import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder import im.vector.app.features.voicebroadcast.VoiceBroadcastRecorder
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState 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.FakeContext
import im.vector.app.test.fakes.FakeRoom import im.vector.app.test.fakes.FakeRoom
import im.vector.app.test.fakes.FakeRoomService 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.clearAllMocks
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.slot import io.mockk.slot
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldBeNull
import org.junit.Test 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.Content
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toContent
@ -48,11 +49,13 @@ class StartVoiceBroadcastUseCaseTest {
private val fakeRoom = FakeRoom() private val fakeRoom = FakeRoom()
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom)) private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true) private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
private val fakeGetOngoingVoiceBroadcastsUseCase = mockk<GetOngoingVoiceBroadcastsUseCase>()
private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase( private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(
fakeSession, session = fakeSession,
fakeVoiceBroadcastRecorder, voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
FakeContext().instance, context = FakeContext().instance,
mockk() buildMeta = mockk(),
getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
) )
@Test @Test
@ -80,7 +83,7 @@ class StartVoiceBroadcastUseCaseTest {
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) { private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
// Given // Given
clearAllMocks() clearAllMocks()
givenAVoiceBroadcasts(voiceBroadcasts) givenVoiceBroadcasts(voiceBroadcasts)
val voiceBroadcastInfoContentInterceptor = slot<Content>() val voiceBroadcastInfoContentInterceptor = slot<Content>()
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID } coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
@ -103,7 +106,7 @@ class StartVoiceBroadcastUseCaseTest {
private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) { private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) {
// Given // Given
clearAllMocks() clearAllMocks()
givenAVoiceBroadcasts(voiceBroadcasts) givenVoiceBroadcasts(voiceBroadcasts)
// When // When
startVoiceBroadcastUseCase.execute(A_ROOM_ID) startVoiceBroadcastUseCase.execute(A_ROOM_ID)
@ -112,7 +115,7 @@ class StartVoiceBroadcastUseCaseTest {
coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) } coVerify(exactly = 0) { fakeRoom.stateService().sendStateEvent(any(), any(), any()) }
} }
private fun givenAVoiceBroadcasts(voiceBroadcasts: List<VoiceBroadcast>) { private fun givenVoiceBroadcasts(voiceBroadcasts: List<VoiceBroadcast>) {
val events = voiceBroadcasts.map { val events = voiceBroadcasts.map {
Event( Event(
type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO, type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
@ -122,7 +125,9 @@ class StartVoiceBroadcastUseCaseTest {
).toContent() ).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) private data class VoiceBroadcast(val userId: String, val state: VoiceBroadcastState)