Merge pull request #8057 from vector-im/yostyle/vb_utd

Let the user know when we are not able to decrypt the voice broadcast…
This commit is contained in:
Benoit Marty 2023-02-07 18:16:34 +01:00 committed by GitHub
commit 0971a28532
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 119 additions and 26 deletions

1
changelog.d/7820.misc Normal file
View file

@ -0,0 +1 @@
Let the user know when we are not able to decrypt the voice broadcast chunks

View file

@ -3120,6 +3120,7 @@
<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="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="error_voice_broadcast_unable_to_play">Unable to play this voice broadcast.</string> <string name="error_voice_broadcast_unable_to_play">Unable to play this voice broadcast.</string>
<string name="error_voice_broadcast_no_connection_recording">Connection error - Recording paused</string> <string name="error_voice_broadcast_no_connection_recording">Connection error - Recording paused</string>
<string name="error_voice_broadcast_unable_to_decrypt">Unable to decrypt this voice broadcast.</string>
<!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left --> <!-- Examples of usage: 6h 15min 30sec left / 15min 30sec left / 30sec left -->
<string name="voice_broadcast_recording_time_left">%1$s left</string> <string name="voice_broadcast_recording_time_left">%1$s left</string>
<string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string> <string name="stop_voice_broadcast_dialog_title">Stop live broadcasting?</string>

View file

@ -28,4 +28,12 @@ interface EventService {
roomId: String, roomId: String,
eventId: String eventId: String
): Event ): Event
/**
* Get an Event from cache. Return null if not found.
*/
fun getEventFromCache(
roomId: String,
eventId: String
): Event?
} }

View file

@ -47,6 +47,12 @@ internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQu
.equalTo(EventEntityFields.EVENT_ID, eventId) .equalTo(EventEntityFields.EVENT_ID, eventId)
} }
internal fun EventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.equalTo(EventEntityFields.ROOM_ID, roomId)
.equalTo(EventEntityFields.EVENT_ID, eventId)
}
internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<EventEntity> { internal fun EventEntity.Companion.whereRoomId(realm: Realm, roomId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>() return realm.where<EventEntity>()
.equalTo(EventEntityFields.ROOM_ID, roomId) .equalTo(EventEntityFields.ROOM_ID, roomId)

View file

@ -18,13 +18,18 @@ package org.matrix.android.sdk.internal.session.events
import org.matrix.android.sdk.api.session.events.EventService import org.matrix.android.sdk.api.session.events.EventService
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.internal.database.RealmSessionProvider
import org.matrix.android.sdk.internal.database.mapper.asDomain
import org.matrix.android.sdk.internal.database.model.EventEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.session.call.CallEventProcessor import org.matrix.android.sdk.internal.session.call.CallEventProcessor
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
import javax.inject.Inject import javax.inject.Inject
internal class DefaultEventService @Inject constructor( internal class DefaultEventService @Inject constructor(
private val getEventTask: GetEventTask, private val getEventTask: GetEventTask,
private val callEventProcessor: CallEventProcessor private val callEventProcessor: CallEventProcessor,
private val realmSessionProvider: RealmSessionProvider,
) : EventService { ) : EventService {
override suspend fun getEvent(roomId: String, eventId: String): Event { override suspend fun getEvent(roomId: String, eventId: String): Event {
@ -36,4 +41,16 @@ internal class DefaultEventService @Inject constructor(
return event return event
} }
override fun getEventFromCache(roomId: String, eventId: String): Event? {
return realmSessionProvider.withRealm { realm ->
EventEntity.where(
realm = realm,
roomId = roomId,
eventId = eventId
)
.findFirst()
?.asDomain()
}
}
} }

View file

@ -160,7 +160,9 @@ class DefaultErrorFormatter @Inject constructor(
RecordingError.BlockedBySomeoneElse -> stringProvider.getString(R.string.error_voice_broadcast_blocked_by_someone_else_message) 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.NoPermission -> stringProvider.getString(R.string.error_voice_broadcast_permission_denied_message)
RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message) RecordingError.UserAlreadyBroadcasting -> stringProvider.getString(R.string.error_voice_broadcast_already_in_progress_message)
is VoiceBroadcastFailure.ListeningError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play) is VoiceBroadcastFailure.ListeningError.UnableToPlay,
is VoiceBroadcastFailure.ListeningError.PrepareMediaPlayerError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play)
is VoiceBroadcastFailure.ListeningError.UnableToDecrypt -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_decrypt)
} }
} }

View file

@ -22,8 +22,12 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.analytics.DecryptionFailureTracker import im.vector.app.features.analytics.DecryptionFailureTracker
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
import im.vector.app.features.voicebroadcast.model.isVoiceBroadcast
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.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -39,6 +43,7 @@ class TimelineItemFactory @Inject constructor(
private val callItemFactory: CallItemFactory, private val callItemFactory: CallItemFactory,
private val decryptionFailureTracker: DecryptionFailureTracker, private val decryptionFailureTracker: DecryptionFailureTracker,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val session: Session,
) { ) {
/** /**
@ -130,11 +135,16 @@ class TimelineItemFactory @Inject constructor(
EventType.CALL_ANSWER -> callItemFactory.create(params) EventType.CALL_ANSWER -> callItemFactory.create(params)
// Crypto // Crypto
EventType.ENCRYPTED -> { EventType.ENCRYPTED -> {
if (event.root.isRedacted()) { val relationContent = event.getRelationContent()
when {
// Redacted event, let the MessageItemFactory handle it // Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(params) event.root.isRedacted() -> messageItemFactory.create(params)
} else { relationContent?.type == RelationType.REFERENCE -> {
encryptedItemFactory.create(params) // Hide the decryption error for VoiceBroadcast chunks
val relatedEvent = relationContent.eventId?.let { session.eventService().getEventFromCache(event.roomId, it) }
if (relatedEvent?.isVoiceBroadcast() != true) encryptedItemFactory.create(params) else null
}
else -> encryptedItemFactory.create(params)
} }
} }
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,

View file

@ -75,6 +75,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
voiceBroadcast = voiceBroadcast, voiceBroadcast = voiceBroadcast,
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
duration = voiceBroadcastEventsGroup.getDuration(), duration = voiceBroadcastEventsGroup.getDuration(),
hasUnableToDecryptEvent = voiceBroadcastEventsGroup.hasUnableToDecryptEvent(),
recorderName = params.event.senderInfo.disambiguatedDisplayName, recorderName = params.event.senderInfo.disambiguatedDisplayName,
recorder = voiceBroadcastRecorder, recorder = voiceBroadcastRecorder,
player = voiceBroadcastPlayer, player = voiceBroadcastPlayer,

View file

@ -25,6 +25,8 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.EventType 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.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
@ -61,6 +63,7 @@ class TimelineEventsGroups {
private fun TimelineEvent.getGroupIdOrNull(): String? { private fun TimelineEvent.getGroupIdOrNull(): String? {
val type = root.getClearType() val type = root.getClearType()
val content = root.getClearContent() val content = root.getClearContent()
val relationContent = root.getRelationContent()
return when { return when {
EventType.isCallEvent(type) -> (content?.get("call_id") as? String) EventType.isCallEvent(type) -> (content?.get("call_id") as? String)
type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> root.asVoiceBroadcastEvent()?.reference?.eventId type == VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO -> root.asVoiceBroadcastEvent()?.reference?.eventId
@ -69,6 +72,9 @@ class TimelineEventsGroups {
// Group voice messages with a reference to an eventId // Group voice messages with a reference to an eventId
root.asMessageAudioEvent()?.getVoiceBroadcastEventId() root.asMessageAudioEvent()?.getVoiceBroadcastEventId()
} }
type == EventType.ENCRYPTED && relationContent?.type == RelationType.REFERENCE -> {
relationContent.eventId
}
else -> { else -> {
null null
} }
@ -153,4 +159,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
fun getDuration(): Int { fun getDuration(): Int {
return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum() return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum()
} }
fun hasUnableToDecryptEvent(): Boolean {
return group.events.any { it.root.getClearType() == EventType.ENCRYPTED }
}
} }

View file

@ -45,6 +45,7 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
protected val player get() = voiceBroadcastAttributes.player protected val player get() = voiceBroadcastAttributes.player
protected val playbackTracker get() = voiceBroadcastAttributes.playbackTracker protected val playbackTracker get() = voiceBroadcastAttributes.playbackTracker
protected val duration get() = voiceBroadcastAttributes.duration protected val duration get() = voiceBroadcastAttributes.duration
protected val hasUnableToDecryptEvent get() = voiceBroadcastAttributes.hasUnableToDecryptEvent
protected val roomItem get() = voiceBroadcastAttributes.roomItem protected val roomItem get() = voiceBroadcastAttributes.roomItem
protected val colorProvider get() = voiceBroadcastAttributes.colorProvider protected val colorProvider get() = voiceBroadcastAttributes.colorProvider
protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider protected val drawableProvider get() = voiceBroadcastAttributes.drawableProvider
@ -102,6 +103,7 @@ abstract class AbsMessageVoiceBroadcastItem<H : AbsMessageVoiceBroadcastItem.Hol
val voiceBroadcast: VoiceBroadcast, val voiceBroadcast: VoiceBroadcast,
val voiceBroadcastState: VoiceBroadcastState?, val voiceBroadcastState: VoiceBroadcastState?,
val duration: Int, val duration: Int,
val hasUnableToDecryptEvent: Boolean,
val recorderName: String, val recorderName: String,
val recorder: VoiceBroadcastRecorder?, val recorder: VoiceBroadcastRecorder?,
val player: VoiceBroadcastPlayer, val player: VoiceBroadcastPlayer,

View file

@ -29,6 +29,7 @@ import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction import im.vector.app.features.home.room.detail.RoomDetailAction.VoiceBroadcastAction
import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker.Listener.State
import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure
import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer
import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.views.VoiceBroadcastBufferingView import im.vector.app.features.voicebroadcast.views.VoiceBroadcastBufferingView
@ -136,12 +137,19 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem
private fun renderPlaybackError(holder: Holder, playbackState: State) { private fun renderPlaybackError(holder: Holder, playbackState: State) {
with(holder) { with(holder) {
if (playbackState is State.Error) { when {
controlsGroup.isVisible = false playbackState is State.Error -> {
errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure)) controlsGroup.isVisible = false
} else { errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure))
errorView.isVisible = false }
controlsGroup.isVisible = true playbackState is State.Idle && hasUnableToDecryptEvent -> {
controlsGroup.isVisible = false
errorView.setTextOrHide(errorFormatter.toHumanReadable(VoiceBroadcastFailure.ListeningError.UnableToDecrypt))
}
else -> {
errorView.isVisible = false
controlsGroup.isVisible = true
}
} }
} }
} }

View file

@ -32,5 +32,6 @@ sealed class VoiceBroadcastFailure : Throwable() {
*/ */
data class UnableToPlay(val what: Int, val extra: Int) : ListeningError() data class UnableToPlay(val what: Int, val extra: Int) : ListeningError()
data class PrepareMediaPlayerError(override val cause: Throwable? = null) : ListeningError() data class PrepareMediaPlayerError(override val cause: Throwable? = null) : ListeningError()
object UnableToDecrypt : ListeningError()
} }
} }

View file

@ -40,7 +40,9 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
import javax.inject.Inject import javax.inject.Inject
@ -189,9 +191,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor(
private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) { private fun fetchPlaylistAndStartPlayback(voiceBroadcast: VoiceBroadcast) {
fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast) fetchPlaylistTask = getLiveVoiceBroadcastChunksUseCase.execute(voiceBroadcast)
.onEach { .onEach { events ->
playlist.setItems(it) if (events.any { it.getClearType() == EventType.ENCRYPTED }) {
onPlaylistUpdated() playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToDecrypt)
} else {
playlist.setItems(events.mapNotNull { it.asMessageAudioEvent() })
onPlaylistUpdated()
}
} }
.launchIn(sessionScope) .launchIn(sessionScope)
} }

View file

@ -33,7 +33,10 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.runningReduce import kotlinx.coroutines.flow.runningReduce
import org.matrix.android.sdk.api.session.events.model.Event
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.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.MessageAudioEvent
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
@ -49,14 +52,22 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventUseCase, private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventUseCase,
) { ) {
fun execute(voiceBroadcast: VoiceBroadcast): Flow<List<MessageAudioEvent>> { fun execute(voiceBroadcast: VoiceBroadcast): Flow<List<Event>> {
val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow() val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow()
val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow() val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow()
val timeline = room.timelineService().createTimeline(null, TimelineSettings(5)) val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
// Get initial chunks // Get initial chunks
val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) val existingChunks = room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId)
.mapNotNull { timelineEvent -> timelineEvent.root.asMessageAudioEvent().takeIf { it.isVoiceBroadcast() } } .mapNotNull { timelineEvent ->
val event = timelineEvent.root
val relationContent = event.getRelationContent()
when {
event.getClearType() == EventType.MESSAGE -> event.takeIf { it.asMessageAudioEvent().isVoiceBroadcast() }
event.getClearType() == EventType.ENCRYPTED && relationContent?.type == RelationType.REFERENCE -> event
else -> null
}
}
val voiceBroadcastEvent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast) val voiceBroadcastEvent = getVoiceBroadcastEventUseCase.execute(voiceBroadcast)
val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState val voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
@ -93,7 +104,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
} }
// Automatically stop observing the timeline if the last chunk has been received // Automatically stop observing the timeline if the last chunk has been received
if (lastSequence != null && newChunks.any { it.sequence == lastSequence }) { if (lastSequence != null && newChunks.any { it.asMessageAudioEvent()?.sequence == lastSequence }) {
timeline.removeListener(this) timeline.removeListener(this)
timeline.dispose() timeline.dispose()
} }
@ -109,8 +120,8 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
timeline.dispose() timeline.dispose()
} }
} }
.runningReduce { accumulator: List<MessageAudioEvent>, value: List<MessageAudioEvent> -> accumulator.plus(value) } .runningReduce { accumulator: List<Event>, value: List<Event> -> accumulator.plus(value) }
.map { events -> events.distinctBy { it.sequence } } .map { events -> events.distinctBy { it.eventId } }
} }
} }
@ -124,12 +135,21 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
/** /**
* Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast. * Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast.
*/ */
private fun List<TimelineEvent>.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List<MessageAudioEvent> = private fun List<TimelineEvent>.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List<Event> =
this.mapNotNull { timelineEvent -> this.mapNotNull { timelineEvent ->
timelineEvent.root.asMessageAudioEvent() val event = timelineEvent.root
?.takeIf { val relationContent = event.getRelationContent()
it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && when {
it.root.senderId == senderId event.getClearType() == EventType.MESSAGE -> {
} event.asMessageAudioEvent()
?.takeIf {
it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId && it.root.senderId == senderId
}?.root
}
event.getClearType() == EventType.ENCRYPTED && relationContent?.type == RelationType.REFERENCE -> {
event.takeIf { relationContent.eventId == voiceBroadcastId }
}
else -> null
}
} }
} }