diff --git a/changelog.d/7820.misc b/changelog.d/7820.misc
new file mode 100644
index 0000000000..1f59cb9afe
--- /dev/null
+++ b/changelog.d/7820.misc
@@ -0,0 +1 @@
+Let the user know when we are not able to decrypt the voice broadcast chunks
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index e690f06bbb..de3fa20916 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -3120,6 +3120,7 @@
You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
Unable to play this voice broadcast.
Connection error - Recording paused
+ Unable to decrypt this voice broadcast.
%1$s left
Stop live broadcasting?
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 0966227917..84f866d1f3 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
@@ -160,7 +160,9 @@ class DefaultErrorFormatter @Inject constructor(
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)
- 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)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
index 61b2385d1d..9bcf3e1b6b 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt
@@ -19,11 +19,17 @@ package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.TimelineEmptyItem
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel
+import im.vector.app.core.extensions.isVoiceBroadcast
import im.vector.app.features.analytics.DecryptionFailureTracker
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants
+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.getRoom
+import org.matrix.android.sdk.api.session.room.getTimelineEvent
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 javax.inject.Inject
@@ -39,6 +45,7 @@ class TimelineItemFactory @Inject constructor(
private val callItemFactory: CallItemFactory,
private val decryptionFailureTracker: DecryptionFailureTracker,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
+ private val session: Session,
) {
/**
@@ -130,9 +137,18 @@ class TimelineItemFactory @Inject constructor(
EventType.CALL_ANSWER -> callItemFactory.create(params)
// Crypto
EventType.ENCRYPTED -> {
+ val relationContent = event.getRelationContent()
if (event.root.isRedacted()) {
// Redacted event, let the MessageItemFactory handle it
messageItemFactory.create(params)
+ } else if (relationContent?.type == RelationType.REFERENCE) {
+ // Hide the decryption error for VoiceBroadcast chunks
+ val startEvent = relationContent.eventId?.let { session.getRoom(event.roomId)?.getTimelineEvent(it) }
+ if (startEvent?.isVoiceBroadcast() == false) {
+ encryptedItemFactory.create(params)
+ } else {
+ null
+ }
} else {
encryptedItemFactory.create(params)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
index 3439fb1f57..7d05463b28 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VoiceBroadcastItemFactory.kt
@@ -75,6 +75,7 @@ class VoiceBroadcastItemFactory @Inject constructor(
voiceBroadcast = voiceBroadcast,
voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState,
duration = voiceBroadcastEventsGroup.getDuration(),
+ hasUnableToDecryptEvent = voiceBroadcastEventsGroup.hasUnableToDecryptEvent(),
recorderName = params.event.senderInfo.disambiguatedDisplayName,
recorder = voiceBroadcastRecorder,
player = voiceBroadcastPlayer,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt
index a4bfa9e155..a3e3f502b6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt
@@ -25,6 +25,8 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState
import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent
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.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.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent
@@ -61,6 +63,7 @@ class TimelineEventsGroups {
private fun TimelineEvent.getGroupIdOrNull(): String? {
val type = root.getClearType()
val content = root.getClearContent()
+ val relationContent = root.getRelationContent()
return when {
EventType.isCallEvent(type) -> (content?.get("call_id") as? String)
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
root.asMessageAudioEvent()?.getVoiceBroadcastEventId()
}
+ type == EventType.ENCRYPTED && relationContent?.type == RelationType.REFERENCE -> {
+ relationContent.eventId
+ }
else -> {
null
}
@@ -153,4 +159,8 @@ class VoiceBroadcastEventsGroup(private val group: TimelineEventsGroup) {
fun getDuration(): Int {
return group.events.mapNotNull { it.root.asMessageAudioEvent()?.duration }.sum()
}
+
+ fun hasUnableToDecryptEvent(): Boolean {
+ return group.events.any { it.root.getClearType() == EventType.ENCRYPTED }
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
index 7cde978e42..21d1abbdf2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageVoiceBroadcastItem.kt
@@ -45,6 +45,7 @@ abstract class AbsMessageVoiceBroadcastItem
+ if (events.any { it.getClearType() == EventType.ENCRYPTED }) {
+ playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToDecrypt)
+ } else {
+ playlist.setItems(events.mapNotNull { it.asMessageAudioEvent() })
+ onPlaylistUpdated()
+ }
}
.launchIn(sessionScope)
}
diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
index 6f7444849a..05a465fb13 100644
--- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
+++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/usecase/GetLiveVoiceBroadcastChunksUseCase.kt
@@ -33,7 +33,10 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
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.getRelationContent
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.timeline.Timeline
@@ -49,14 +52,22 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
private val getVoiceBroadcastEventUseCase: GetVoiceBroadcastStateEventUseCase,
) {
- fun execute(voiceBroadcast: VoiceBroadcast): Flow> {
+ fun execute(voiceBroadcast: VoiceBroadcast): Flow> {
val session = activeSessionHolder.getSafeActiveSession() ?: return emptyFlow()
val room = session.roomService().getRoom(voiceBroadcast.roomId) ?: return emptyFlow()
val timeline = room.timelineService().createTimeline(null, TimelineSettings(5))
// Get initial chunks
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 voiceBroadcastState = voiceBroadcastEvent?.content?.voiceBroadcastState
@@ -93,7 +104,7 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
}
// 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.dispose()
}
@@ -109,8 +120,8 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
timeline.dispose()
}
}
- .runningReduce { accumulator: List, value: List -> accumulator.plus(value) }
- .map { events -> events.distinctBy { it.sequence } }
+ .runningReduce { accumulator: List, value: List -> accumulator.plus(value) }
+ .map { events -> events.distinctBy { it.eventId } }
}
}
@@ -124,12 +135,19 @@ class GetLiveVoiceBroadcastChunksUseCase @Inject constructor(
/**
* Transform the list of [TimelineEvent] to a mapped list of [MessageAudioEvent] related to a given voice broadcast.
*/
- private fun List.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List =
+ private fun List.mapToChunkEvents(voiceBroadcastId: String, senderId: String?): List =
this.mapNotNull { timelineEvent ->
- timelineEvent.root.asMessageAudioEvent()
- ?.takeIf {
- it.isVoiceBroadcast() && it.getVoiceBroadcastEventId() == voiceBroadcastId &&
- it.root.senderId == senderId
- }
+ val event = timelineEvent.root
+ val relationContent = event.getRelationContent()
+ when {
+ 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
+ else -> null
+ }
}
}