Merge pull request #7485 from vector-im/feature/fre/start_voice_broadcast_error
Voice Broadcast - Show start voice broadcast errors in a dialog
This commit is contained in:
commit
48cca9973b
|
@ -0,0 +1 @@
|
|||
[Voice Broadcast] Display an error dialog if the user fails to start a voice broadcast
|
|
@ -3085,6 +3085,10 @@
|
|||
<string name="a11y_play_voice_broadcast">Play or resume voice broadcast</string>
|
||||
<string name="a11y_pause_voice_broadcast">Pause voice broadcast</string>
|
||||
<string name="a11y_voice_broadcast_buffering">Buffering</string>
|
||||
<string name="error_voice_broadcast_unauthorized_title">Can’t start a new voice broadcast</string>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()) {
|
||||
assertCanStartVoiceBroadcast(room)
|
||||
startVoiceBroadcast(room)
|
||||
} else {
|
||||
Timber.d("## StartVoiceBroadcastUseCase: Cannot start voice broadcast: currentVoiceBroadcastEvents=$onGoingVoiceBroadcastEvents")
|
||||
}
|
||||
}
|
||||
|
||||
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<PowerLevelsContent>()
|
||||
?.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,13 +53,22 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
private val fakeSession = FakeSession(fakeRoomService = FakeRoomService(fakeRoom))
|
||||
private val fakeVoiceBroadcastRecorder = mockk<VoiceBroadcastRecorder>(relaxed = true)
|
||||
private val fakeGetOngoingVoiceBroadcastsUseCase = mockk<GetOngoingVoiceBroadcastsUseCase>()
|
||||
private val startVoiceBroadcastUseCase = StartVoiceBroadcastUseCase(
|
||||
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 {
|
||||
|
@ -83,7 +94,7 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
|
||||
private suspend fun testVoiceBroadcastStarted(voiceBroadcasts: List<VoiceBroadcast>) {
|
||||
// Given
|
||||
clearAllMocks()
|
||||
setup()
|
||||
givenVoiceBroadcasts(voiceBroadcasts)
|
||||
val voiceBroadcastInfoContentInterceptor = slot<Content>()
|
||||
coEvery { fakeRoom.stateService().sendStateEvent(any(), any(), capture(voiceBroadcastInfoContentInterceptor)) } coAnswers { AN_EVENT_ID }
|
||||
|
@ -106,7 +117,7 @@ class StartVoiceBroadcastUseCaseTest {
|
|||
|
||||
private suspend fun testVoiceBroadcastNotStarted(voiceBroadcasts: List<VoiceBroadcast>) {
|
||||
// Given
|
||||
clearAllMocks()
|
||||
setup()
|
||||
givenVoiceBroadcasts(voiceBroadcasts)
|
||||
|
||||
// When
|
||||
|
|
Loading…
Reference in New Issue