Skip to content

Commit 48cca99

Browse files
authored
Merge pull request #7485 from vector-im/feature/fre/start_voice_broadcast_error
Voice Broadcast - Show start voice broadcast errors in a dialog
2 parents 01ab39e + d779140 commit 48cca99

File tree

8 files changed

+115
-18
lines changed

8 files changed

+115
-18
lines changed

changelog.d/7485.wip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast

library/ui-strings/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3085,6 +3085,10 @@
30853085
<string name="a11y_play_voice_broadcast">Play or resume voice broadcast</string>
30863086
<string name="a11y_pause_voice_broadcast">Pause voice broadcast</string>
30873087
<string name="a11y_voice_broadcast_buffering">Buffering</string>
3088+
<string name="error_voice_broadcast_unauthorized_title">Can’t start a new voice broadcast</string>
3089+
<string name="error_voice_broadcast_permission_denied_message">You don’t have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.</string>
3090+
<string name="error_voice_broadcast_blocked_by_someone_else_message">Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.</string>
3091+
<string name="error_voice_broadcast_already_in_progress_message">You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.</string>
30883092

30893093
<string name="upgrade_room_for_restricted">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.</string>
30903094
<string name="upgrade_room_for_restricted_no_param">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.</string>

vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import im.vector.app.R
2121
import im.vector.app.core.resources.StringProvider
2222
import im.vector.app.features.call.dialpad.DialPadLookup
2323
import im.vector.app.features.voice.VoiceFailure
24+
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
25+
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure.RecordingError
2426
import org.matrix.android.sdk.api.failure.Failure
2527
import org.matrix.android.sdk.api.failure.MatrixError
2628
import org.matrix.android.sdk.api.failure.MatrixIdFailure
@@ -135,6 +137,7 @@ class DefaultErrorFormatter @Inject constructor(
135137
is MatrixIdFailure.InvalidMatrixId ->
136138
stringProvider.getString(R.string.login_signin_matrix_id_error_invalid_matrix_id)
137139
is VoiceFailure -> voiceMessageError(throwable)
140+
is VoiceBroadcastFailure -> voiceBroadcastMessageError(throwable)
138141
is ActivityNotFoundException ->
139142
stringProvider.getString(R.string.error_no_external_application_found)
140143
else -> throwable.localizedMessage
@@ -149,6 +152,14 @@ class DefaultErrorFormatter @Inject constructor(
149152
}
150153
}
151154

155+
private fun voiceBroadcastMessageError(throwable: VoiceBroadcastFailure): String {
156+
return when (throwable) {
157+
RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message)
158+
RecordingError.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
159+
RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
160+
}
161+
}
162+
152163
private fun limitExceededError(error: MatrixError): String {
153164
val delay = error.retryAfterMillis
154165

vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import android.widget.FrameLayout
3333
import android.widget.ImageView
3434
import android.widget.TextView
3535
import androidx.activity.addCallback
36+
import androidx.annotation.StringRes
3637
import androidx.appcompat.view.menu.MenuBuilder
3738
import androidx.constraintlayout.widget.ConstraintSet
3839
import androidx.core.content.ContextCompat
@@ -1320,8 +1321,12 @@ class TimelineFragment :
13201321
}
13211322

13221323
private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) {
1324+
@StringRes val titleResId = when (result.action) {
1325+
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> R.string.error_voice_broadcast_unauthorized_title
1326+
else -> R.string.dialog_title_error
1327+
}
13231328
MaterialAlertDialogBuilder(requireActivity())
1324-
.setTitle(R.string.dialog_title_error)
1329+
.setTitle(titleResId)
13251330
.setMessage(errorFormatter.toHumanReadable(result.throwable))
13261331
.setPositiveButton(R.string.ok, null)
13271332
.show()

vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,12 @@ class TimelineViewModel @AssistedInject constructor(
604604
if (room == null) return
605605
viewModelScope.launch {
606606
when (action) {
607-
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> voiceBroadcastHelper.startVoiceBroadcast(room.roomId)
607+
RoomDetailAction.VoiceBroadcastAction.Recording.Start -> {
608+
voiceBroadcastHelper.startVoiceBroadcast(room.roomId).fold(
609+
{ _viewEvents.post(RoomDetailViewEvents.ActionSuccess(action)) },
610+
{ _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, it)) },
611+
)
612+
}
608613
RoomDetailAction.VoiceBroadcastAction.Recording.Pause -> voiceBroadcastHelper.pauseVoiceBroadcast(room.roomId)
609614
RoomDetailAction.VoiceBroadcastAction.Recording.Resume -> voiceBroadcastHelper.resumeVoiceBroadcast(room.roomId)
610615
RoomDetailAction.VoiceBroadcastAction.Recording.Stop -> voiceBroadcastHelper.stopVoiceBroadcast(room.roomId)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.voicebroadcast
18+
19+
sealed class VoiceBroadcastFailure : Throwable() {
20+
sealed class RecordingError : VoiceBroadcastFailure() {
21+
object NoPermission : RecordingError()
22+
object BlockedBySomeoneElse : RecordingError()
23+
object UserAlreadyBroadcasting : RecordingError()
24+
}
25+
}

vector/src/main/java/im/vector/app/features/voicebroadcast/recording/usecase/StartVoiceBroadcastUseCase.kt

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,26 @@ import androidx.core.content.FileProvider
2121
import im.vector.app.core.resources.BuildMeta
2222
import im.vector.app.features.attachments.toContentAttachmentData
2323
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
24+
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
2425
import im.vector.app.features.voicebroadcast.model.MessageVoiceBroadcastInfoContent
2526
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastChunk
2627
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
2728
import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder
2829
import im.vector.app.features.voicebroadcast.usecase.GetOngoingVoiceBroadcastsUseCase
2930
import im.vector.lib.multipicker.utils.toMultiPickerAudioType
31+
import org.jetbrains.annotations.VisibleForTesting
32+
import org.matrix.android.sdk.api.query.QueryStringValue
3033
import org.matrix.android.sdk.api.session.Session
34+
import org.matrix.android.sdk.api.session.events.model.EventType
3135
import org.matrix.android.sdk.api.session.events.model.RelationType
3236
import org.matrix.android.sdk.api.session.events.model.toContent
37+
import org.matrix.android.sdk.api.session.events.model.toModel
3338
import org.matrix.android.sdk.api.session.getRoom
3439
import org.matrix.android.sdk.api.session.room.Room
40+
import org.matrix.android.sdk.api.session.room.getStateEvent
41+
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
3542
import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent
43+
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
3644
import timber.log.Timber
3745
import java.io.File
3846
import javax.inject.Inject
@@ -50,13 +58,8 @@ class StartVoiceBroadcastUseCase @Inject constructor(
5058

5159
Timber.d("## StartVoiceBroadcastUseCase: Start voice broadcast requested")
5260

53-
val onGoingVoiceBroadcastEvents = getOngoingVoiceBroadcastsUseCase.execute(roomId)
54-
55-
if (onGoingVoiceBroadcastEvents.isEmpty()) {
56-
startVoiceBroadcast(room)
57-
} else {
58-
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: currentVoiceBroadcastEvents=$onGoingVoiceBroadcastEvents")
59-
}
61+
assertCanStartVoiceBroadcast(room)
62+
startVoiceBroadcast(room)
6063
}
6164

6265
private suspend fun startVoiceBroadcast(room: Room) {
@@ -102,4 +105,36 @@ class StartVoiceBroadcastUseCase @Inject constructor(
102105
)
103106
)
104107
}
108+
109+
private fun assertCanStartVoiceBroadcast(room: Room) {
110+
assertHasEnoughPowerLevels(room)
111+
assertNoOngoingVoiceBroadcast(room)
112+
}
113+
114+
@VisibleForTesting
115+
fun assertHasEnoughPowerLevels(room: Room) {
116+
val powerLevelsHelper = room.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.IsEmpty)
117+
?.content
118+
?.toModel<PowerLevelsContent>()
119+
?.let { PowerLevelsHelper(it) }
120+
121+
if (powerLevelsHelper?.isUserAllowedToSend(session.myUserId, true, VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO) != true) {
122+
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: no permission")
123+
throw VoiceBroadcastFailure.RecordingError.NoPermission
124+
}
125+
}
126+
127+
@VisibleForTesting
128+
fun assertNoOngoingVoiceBroadcast(room: Room) {
129+
when {
130+
voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Recording || voiceBroadcastRecorder?.state == VoiceBroadcastRecorder.State.Paused -> {
131+
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: another voice broadcast")
132+
throw VoiceBroadcastFailure.RecordingError.UserAlreadyBroadcasting
133+
}
134+
getOngoingVoiceBroadcastsUseCase.execute(room.roomId).isNotEmpty() -> {
135+
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: user already broadcasting")
136+
throw VoiceBroadcastFailure.RecordingError.BlockedBySomeoneElse
137+
}
138+
}
139+
}
105140
}

vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/StartVoiceBroadcastUseCaseTest.kt

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,17 @@ import im.vector.app.test.fakes.FakeContext
2626
import im.vector.app.test.fakes.FakeRoom
2727
import im.vector.app.test.fakes.FakeRoomService
2828
import im.vector.app.test.fakes.FakeSession
29-
import io.mockk.clearAllMocks
3029
import io.mockk.coEvery
3130
import io.mockk.coVerify
3231
import io.mockk.every
32+
import io.mockk.justRun
3333
import io.mockk.mockk
3434
import io.mockk.slot
35+
import io.mockk.spyk
3536
import kotlinx.coroutines.test.runTest
3637
import org.amshove.kluent.shouldBe
3738
import org.amshove.kluent.shouldBeNull
39+
import org.junit.Before
3840
import org.junit.Test
3941
import org.matrix.android.sdk.api.session.events.model.Content
4042
import org.matrix.android.sdk.api.session.events.model.Event
@@ -51,14 +53,23 @@ class StartVoiceBroadcastUseCaseTest {
5153
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
5254
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
5355
private val fakeGetOngoingVoiceBroadcastsUseCase = mockk<GetOngoingVoiceBroadcastsUseCase>()
54-
private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(
55-
session = fakeSession,
56-
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
57-
context = FakeContext().instance,
58-
buildMeta = mockk(),
59-
getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
56+
private val startVoiceBroadcastUseCase = spyk(
57+
StartVoiceBroadcastUseCase(
58+
session = fakeSession,
59+
voiceBroadcastRecorder = fakeVoiceBroadcastRecorder,
60+
context = FakeContext().instance,
61+
buildMeta = mockk(),
62+
getOngoingVoiceBroadcastsUseCase = fakeGetOngoingVoiceBroadcastsUseCase,
63+
)
6064
)
6165

66+
@Before
67+
fun setup() {
68+
every { fakeRoom.roomId } returns A_ROOM_ID
69+
justRun { startVoiceBroadcastUseCase.assertHasEnoughPowerLevels(fakeRoom) }
70+
every { fakeVoiceBroadcastRecorder.state } returns VoiceBroadcastRecorder.State.Idle
71+
}
72+
6273
@Test
6374
fun `given a room id with potential several existing voice broadcast states when calling execute then the voice broadcast is started or not`() = runTest {
6475
val cases = VoiceBroadcastState.values()
@@ -83,7 +94,7 @@ class StartVoiceBroadcastUseCaseTest {
8394

8495
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
8596
// Given
86-
clearAllMocks()
97+
setup()
8798
givenVoiceBroadcasts(voiceBroadcasts)
8899
val voiceBroadcastInfoContentInterceptor = slot<Content>()
89100
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
@@ -106,7 +117,7 @@ class StartVoiceBroadcastUseCaseTest {
106117

107118
private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) {
108119
// Given
109-
clearAllMocks()
120+
setup()
110121
givenVoiceBroadcasts(voiceBroadcasts)
111122

112123
// When

0 commit comments

Comments
 (0)