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:
commit
6ee77ad101
1
changelog.d/7450.wip
Normal file
1
changelog.d/7450.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Voice Broadcast] Stop recording when opening the room after an app restart
|
@ -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 ->
|
||||||
|
@ -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()))
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user