Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Voice Broadcast - Stop recording on app restart #7450

Merged
Merged
1 change: 1 addition & 0 deletions changelog.d/7450.wip
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Voice Broadcast] Stop recording when opening the room after an app restart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<HomeActivityViewState, HomeActivityViewActions, HomeActivityViewEvents>(initialState) {

@AssistedFactory
Expand Down Expand Up @@ -123,6 +125,7 @@ class HomeActivityViewModel @AssistedInject constructor(
observeReleaseNotes()
observeLocalNotificationsSilenced()
initThreadsMigration()
viewModelScope.launch { stopOngoingVoiceBroadcastUseCase.execute() }
}

private fun observeReleaseNotes() = withState { state ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ class MessageComposerFragment : VectorBaseFragment<FragmentComposerBinding>(), 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()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove this println? If we want to keep some logs, we should use Timber instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch... will create a new PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering if we should use getStateEvent() instead. I think the issue may only happen for the last emitted state event of the room right? If we use getStateEvents it will go through every saved state events which may be a big list in large room.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mnaturel Not sure, in Element, we cannot have more than one ongoing VB in a room. But "advanced users" or other clients are still allowed to send new state events with different state keys and we'll have several ongoing VB from several users...

setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO),
QueryStringValue.IsNotEmpty
)
.mapNotNull { it.asVoiceBroadcastEvent() }
.filter { it.content?.voiceBroadcastState != null && it.content?.voiceBroadcastState != VoiceBroadcastState.STOPPED }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -43,19 +41,15 @@ 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<Unit> = runCatching {
val room = session.getRoom(roomId) ?: error("Unknown roomId: $roomId")

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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,21 @@ 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
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
Expand All @@ -48,11 +49,13 @@ class StartVoiceBroadcastUseCaseTest {
private val fakeRoom = FakeRoom()
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
private val fakeGetOngoingVoiceBroadcastsUseCase = mockk<GetOngoingVoiceBroadcastsUseCase>()
private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(
fakeSession,
fakeVoiceBroadcastRecorder,
FakeContext().instance,
mockk()
session = fakeSession,
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
context = FakeContext().instance,
buildMeta = mockk(),
getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
)

@Test
Expand Down Expand Up @@ -80,7 +83,7 @@ class StartVoiceBroadcastUseCaseTest {
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
// Given
clearAllMocks()
givenAVoiceBroadcasts(voiceBroadcasts)
givenVoiceBroadcasts(voiceBroadcasts)
val voiceBroadcastInfoContentInterceptor = slot<Content>()
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }

Expand All @@ -103,7 +106,7 @@ class StartVoiceBroadcastUseCaseTest {
private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) {
// Given
clearAllMocks()
givenAVoiceBroadcasts(voiceBroadcasts)
givenVoiceBroadcasts(voiceBroadcasts)

// When
startVoiceBroadcastUseCase.execute(A_ROOM_ID)
Expand All @@ -112,7 +115,7 @@ class StartVoiceBroadcastUseCaseTest {
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 {
Event(
type = VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO,
Expand All @@ -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)
Expand Down