diff --git a/changelog.d/7485.wip b/changelog.d/7485.wip
new file mode 100644
index 0000000000..30cab45d9c
--- /dev/null
+++ b/changelog.d/7485.wip
@@ -0,0 +1 @@
+[Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 9edd7d836a..b5abefec94 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3085,6 +3085,10 @@
Play or resume voice broadcast
Pause voice broadcast
Buffering
+ Can’t start a new voice broadcast
+ You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.
+ Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.
+ You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
Anyone in %s will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.
Anyone in a parent space will be able to find and join this room - no need to manually invite everyone. You’ll be able to change this in room settings anytime.
diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
index a09f852958..380c80775b 100644
--- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
+++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
@@ -21,6 +21,8 @@ import im.vector.app.R
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.voice.VoiceFailure
+import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
+import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.MatrixIdFailure
@@ -135,6 +137,7 @@ class DefaultErrorFormatter @Inject constructor(
is MatrixIdFailure.InvalidMatrixId ->
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
is VoiceFailure -> voiceMessageError(throwable)
+ is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
is ActivityNotFoundException ->
stringProvider.getString(R.string.error_no_external_application_found)
else -> throwable.localizedMessage
@@ -149,6 +152,14 @@ class DefaultErrorFormatter @Inject constructor(
}
}
+ private fun voiceBroadcastMessageError(throwable: VoiceBroadcastFailure): String {
+ return when (throwable) {
+ RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message)
+ RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
+ RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
+ }
+ }
+
private fun limitExceededError(error: MatrixError): String {
val delay = error.retryAfterMillis
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 4f51922a62..120e5e22cb 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -33,6 +33,7 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.addCallback
+import androidx.annotation.StringRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.content.ContextCompat
@@ -1320,8 +1321,12 @@ class TimelineFragment :
}
private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
+ @StringRes val titleResId = when (result.action) {
+ RoomDetailAction.VoiceBroadcastAction.Recording.Start -> R.string.error_voice_broadcast_unauthorized_title
+ else -> R.string.dialog_title_error
+ }
MaterialAlertDialogBuilder(requireActivity())
- .setTitle(R.string.dialog_title_error)
+ .setTitle(titleResId)
.setMessage(errorFormatter.toHumanReadable(result.throwable))
.setPositiveButton(R.string.ok, null)
.show()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
index 82ad96d645..ac117558be 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt
@@ -604,7 +604,12 @@ class TimelineViewModel @AssistedInject constructor(
if (room == null) return
viewModelScope.launch {
when (action) {
- RoomDetailAction.VoiceBroadcastAction.Recording.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId)
+ RoomDetailAction.VoiceBroadcastAction.Recording.Start -> {
+ voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold(
+ { _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) },
+ { _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) },
+ )
+ }
RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt
new file mode 100644
index 0000000000..76b50c78ab
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+sealed class VoiceBroadcastFailure : Throwable() {
+ sealed class RecordingError : VoiceBroadcastFailure() {
+ object NoPermission : RecordingError()
+ object BlockedBySomeoneElse : RecordingError()
+ object UserAlreadyBroadcasting : RecordingError()
+ }
+}
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 a1a519a656..85f72c09da 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
@@ -21,18 +21,26 @@ import androidx.core.content.FileProvider
import im.vector.app.core.resources.BuildMeta
import im.vector.app.features.attachments.toContentAttachmentData
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.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 org.jetbrains.annotations.VisibleForTesting
+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.EventType
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.toModel
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.Room
+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 timber.log.Timber
import java.io.File
import javax.inject.Inject
@@ -50,13 +58,8 @@ class StartVoiceBroadcastUseCase @Inject constructor(
Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested")
- val onGoingVoiceBroadcastEvents = getOngoingVoiceBroadcastsUseCase.execute(roomId)
-
- if (onGoingVoiceBroadcastEvents.isEmpty()) {
- startVoiceBroadcast(room)
- } else {
- Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: currentVoiceBroadcastEvents=$onGoingVoiceBroadcastEvents")
- }
+ assertCanStartVoiceBroadcast(room)
+ startVoiceBroadcast(room)
}
private suspend fun startVoiceBroadcast(room: Room) {
@@ -102,4 +105,36 @@ class StartVoiceBroadcastUseCase @Inject constructor(
)
)
}
+
+ private fun assertCanStartVoiceBroadcast(room: Room) {
+ assertHasEnoughPowerLevels(room)
+ assertNoOngoingVoiceBroadcast(room)
+ }
+
+ @VisibleForTesting
+ fun assertHasEnoughPowerLevels(room: Room) {
+ val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
+ ?.content
+ ?.toModel()
+ ?.let { PowerLevelsHelper(it) }
+
+ if (powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true) {
+ Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission")
+ throw VoiceBroadcastFailure.RecordingError.NoPermission
+ }
+ }
+
+ @VisibleForTesting
+ fun assertNoOngoingVoiceBroadcast(room: Room) {
+ when {
+ voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> {
+ Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast")
+ throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting
+ }
+ getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> {
+ Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting")
+ throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse
+ }
+ }
+ }
}
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 59929ef0d7..ef78f1c80d 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
@@ -26,15 +26,17 @@ 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.justRun
import io.mockk.mockk
import io.mockk.slot
+import io.mockk.spyk
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBe
import org.amshove.kluent.shouldBeNull
+import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.events.model.Content
import org.matrix.android.sdk.api.session.events.model.Event
@@ -51,14 +53,23 @@ class StartVoiceBroadcastUseCaseTest {
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
private val fakeVoiceBroadcastRecorder = mockk(relaxed = true)
private val fakeGetOngoingVoiceBroadcastsUseCase = mockk()
- private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(
- session = fakeSession,
- voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
- context = FakeContext().instance,
- buildMeta = mockk(),
- getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
+ private val startVoiceBroadcastUseCase = spyk(
+ StartVoiceBroadcastUseCase(
+ session = fakeSession,
+ voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
+ context = FakeContext().instance,
+ buildMeta = mockk(),
+ getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
+ )
)
+ @Before
+ fun setup() {
+ every { fakeRoom.roomId } returns A_ROOM_ID
+ justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) }
+ every { fakeVoiceBroadcastRecorder.state } returns VoiceBroadcastRecorder.State.Idle
+ }
+
@Test
fun `given a room id with potential several existing voice broadcast states when calling execute then the voice broadcast is started or not`() = runTest {
val cases = VoiceBroadcastState.values()
@@ -83,7 +94,7 @@ class StartVoiceBroadcastUseCaseTest {
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List) {
// Given
- clearAllMocks()
+ setup()
givenVoiceBroadcasts(voiceBroadcasts)
val voiceBroadcastInfoContentInterceptor = slot()
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
@@ -106,7 +117,7 @@ class StartVoiceBroadcastUseCaseTest {
private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List) {
// Given
- clearAllMocks()
+ setup()
givenVoiceBroadcasts(voiceBroadcasts)
// When