From f8852856c6d60c6b9245d47b2393df5ed08dabc5 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 10 Jan 2023 10:34:45 +0100 Subject: [PATCH 01/75] Convert state enum to sealed interface --- .../MessageVoiceBroadcastListeningItem.kt | 20 +++--- .../listening/VoiceBroadcastPlayer.kt | 12 ++-- .../listening/VoiceBroadcastPlayerImpl.kt | 62 +++++++++---------- 3 files changed, 47 insertions(+), 47 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index b788d79214..d21e6771d9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -63,10 +63,10 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem playPauseButton.setOnClickListener { if (player.currentVoiceBroadcast == voiceBroadcast) { when (player.playingState) { - VoiceBroadcastPlayer.State.PLAYING, - VoiceBroadcastPlayer.State.BUFFERING -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) - VoiceBroadcastPlayer.State.PAUSED, - VoiceBroadcastPlayer.State.IDLE -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) + VoiceBroadcastPlayer.State.Playing, + VoiceBroadcastPlayer.State.Buffering -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) + VoiceBroadcastPlayer.State.Paused, + VoiceBroadcastPlayer.State.Idle -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } } else { callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) @@ -100,17 +100,17 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem private fun renderPlayingState(holder: Holder, state: VoiceBroadcastPlayer.State) { with(holder) { - bufferingView.isVisible = state == VoiceBroadcastPlayer.State.BUFFERING - voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.BUFFERING + bufferingView.isVisible = state == VoiceBroadcastPlayer.State.Buffering + voiceBroadcastMetadata.isVisible = state != VoiceBroadcastPlayer.State.Buffering when (state) { - VoiceBroadcastPlayer.State.PLAYING, - VoiceBroadcastPlayer.State.BUFFERING -> { + VoiceBroadcastPlayer.State.Playing, + VoiceBroadcastPlayer.State.Buffering -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) } - VoiceBroadcastPlayer.State.IDLE, - VoiceBroadcastPlayer.State.PAUSED -> { + VoiceBroadcastPlayer.State.Idle, + VoiceBroadcastPlayer.State.Paused -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_play_voice_broadcast) } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 0de88e9992..5e5c320d43 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -26,7 +26,7 @@ interface VoiceBroadcastPlayer { val currentVoiceBroadcast: VoiceBroadcast? /** - * The current playing [State], [State.IDLE] by default. + * The current playing [State], [State.Idle] by default. */ val playingState: State @@ -68,11 +68,11 @@ interface VoiceBroadcastPlayer { /** * Player states. */ - enum class State { - PLAYING, - PAUSED, - BUFFERING, - IDLE + sealed interface State { + object Playing : State + object Paused : State + object Buffering : State + object Idle : State } /** diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 9cb894bb58..f00d657682 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -79,7 +79,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } - override var playingState = State.IDLE + override var playingState: State = State.Idle @MainThread set(value) { if (field != value) { @@ -96,7 +96,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val hasChanged = currentVoiceBroadcast != voiceBroadcast when { hasChanged -> startPlayback(voiceBroadcast) - playingState == State.PAUSED -> resumePlayback() + playingState == State.Paused -> resumePlayback() else -> Unit } } @@ -107,7 +107,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun stop() { // Update state - playingState = State.IDLE + playingState = State.Idle // Stop and release media players stopPlayer() @@ -129,7 +129,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( listeners[voiceBroadcast.voiceBroadcastId]?.add(listener) ?: run { listeners[voiceBroadcast.voiceBroadcastId] = CopyOnWriteArrayList().apply { add(listener) } } - listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.IDLE) + listener.onPlayingStateChanged(if (voiceBroadcast == currentVoiceBroadcast) playingState else State.Idle) listener.onLiveModeChanged(voiceBroadcast == currentVoiceBroadcast) } @@ -139,11 +139,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun startPlayback(voiceBroadcast: VoiceBroadcast) { // Stop listening previous voice broadcast if any - if (playingState != State.IDLE) stop() + if (playingState != State.Idle) stop() currentVoiceBroadcast = voiceBroadcast - playingState = State.BUFFERING + playingState = State.Buffering observeVoiceBroadcastStateEvent(voiceBroadcast) } @@ -175,13 +175,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onPlaylistUpdated() { when (playingState) { - State.PLAYING, - State.PAUSED -> { + State.Playing, + State.Paused -> { if (nextMediaPlayer == null && !isPreparingNextPlayer) { prepareNextMediaPlayer() } } - State.BUFFERING -> { + State.Buffering -> { val nextItem = if (isLiveListening && playlist.currentSequence == null) { // live listening, jump to the last item if playback has not started playlist.lastOrNull() @@ -193,7 +193,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( startPlayback(nextItem.startTime) } } - State.IDLE -> Unit // Should not happen + State.Idle -> Unit // Should not happen } } @@ -213,7 +213,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (sequencePosition > 0) { mp.seekTo(sequencePosition) } - playingState = State.PLAYING + playingState = State.Playing prepareNextMediaPlayer() } } catch (failure: Throwable) { @@ -224,7 +224,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } private fun pausePlayback() { - playingState = State.PAUSED // This will trigger a playing state update and save the current position + playingState = State.Paused // This will trigger a playing state update and save the current position if (currentMediaPlayer != null) { currentMediaPlayer?.pause() } else { @@ -234,7 +234,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun resumePlayback() { if (currentMediaPlayer != null) { - playingState = State.PLAYING + playingState = State.Playing currentMediaPlayer?.start() } else { val savedPosition = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPlaybackTime(it) } ?: 0 @@ -247,11 +247,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( voiceBroadcast != currentVoiceBroadcast -> { playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration) } - playingState == State.PLAYING || playingState == State.BUFFERING -> { + playingState == State.Playing || playingState == State.Buffering -> { updateLiveListeningMode(positionMillis) startPlayback(positionMillis) } - playingState == State.IDLE || playingState == State.PAUSED -> { + playingState == State.Idle || playingState == State.Paused -> { stopPlayer() playbackTracker.updatePausedAtPlaybackTime(voiceBroadcast.voiceBroadcastId, positionMillis, positionMillis.toFloat() / duration) } @@ -267,15 +267,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor( isPreparingNextPlayer = false nextMediaPlayer = mp when (playingState) { - State.PLAYING, - State.PAUSED -> { + State.Playing, + State.Paused -> { currentMediaPlayer?.setNextMediaPlayer(mp) } - State.BUFFERING -> { + State.Buffering -> { mp.start() onNextMediaPlayerStarted(mp) } - State.IDLE -> stopPlayer() + State.Idle -> stopPlayer() } } } @@ -327,10 +327,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( currentVoiceBroadcast?.voiceBroadcastId?.let { voiceBroadcastId -> // Start or stop playback ticker when (playingState) { - State.PLAYING -> playbackTicker.startPlaybackTicker(voiceBroadcastId) - State.PAUSED, - State.BUFFERING, - State.IDLE -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) + State.Playing -> playbackTicker.startPlaybackTicker(voiceBroadcastId) + State.Paused, + State.Buffering, + State.Idle -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) } // Notify state change to all the listeners attached to the current voice broadcast id listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) } @@ -348,7 +348,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // the current voice broadcast is not live (ended) mostRecentVoiceBroadcastEvent?.isLive != true -> false // the player is stopped or paused - playingState == State.IDLE || playingState == State.PAUSED -> false + playingState == State.Idle || playingState == State.Paused -> false seekPosition != null -> { val seekDirection = seekPosition.compareTo(getCurrentPlaybackPosition() ?: 0) val newSequence = playlist.findByPosition(seekPosition)?.sequence @@ -374,13 +374,13 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onLiveListeningChanged(isLiveListening: Boolean) { // Live has ended and last chunk has been reached, we can stop the playback - if (!isLiveListening && playingState == State.BUFFERING && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) { + if (!isLiveListening && playingState == State.Buffering && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) { stop() } } private fun onNextMediaPlayerStarted(mp: MediaPlayer) { - playingState = State.PLAYING + playingState = State.Playing playlist.currentSequence = playlist.currentSequence?.inc() currentMediaPlayer = mp nextMediaPlayer = null @@ -427,7 +427,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( stop() } else { // Enter in buffering mode and release current media player - playingState = State.BUFFERING + playingState = State.Buffering currentMediaPlayer?.release() currentMediaPlayer = null } @@ -462,18 +462,18 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val playbackTime = getCurrentPlaybackPosition() val percentage = getCurrentPlaybackPercentage() when (playingState) { - State.PLAYING -> { + State.Playing -> { if (playbackTime != null && percentage != null) { playbackTracker.updatePlayingAtPlaybackTime(id, playbackTime, percentage) } } - State.PAUSED, - State.BUFFERING -> { + State.Paused, + State.Buffering -> { if (playbackTime != null && percentage != null) { playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) } } - State.IDLE -> { + State.Idle -> { if (playbackTime == null || percentage == null || (playlist.duration - playbackTime) < 50) { playbackTracker.stopPlayback(id) } else { From 2d24eb1273d0aa72e9ea2e5f758484763628af34 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 10 Jan 2023 14:00:46 +0100 Subject: [PATCH 02/75] Handle playback error --- changelog.d/7829.bugfix | 1 + .../src/main/res/values/strings.xml | 1 + .../vector/app/core/error/ErrorFormatter.kt | 2 + .../voice/VoiceMessageRecorderView.kt | 1 + .../factory/VoiceBroadcastItemFactory.kt | 3 + .../helper/AudioMessagePlaybackTracker.kt | 30 ++++++--- .../item/AbsMessageVoiceBroadcastItem.kt | 3 + .../detail/timeline/item/MessageAudioItem.kt | 1 + .../MessageVoiceBroadcastListeningItem.kt | 35 ++++++++--- .../detail/timeline/item/MessageVoiceItem.kt | 1 + .../voicebroadcast/VoiceBroadcastFailure.kt | 11 ++++ .../listening/VoiceBroadcastPlayer.kt | 2 + .../listening/VoiceBroadcastPlayerImpl.kt | 63 +++++++++++-------- .../res/drawable/ic_voice_broadcast_error.xml | 10 +++ ...e_event_voice_broadcast_listening_stub.xml | 23 +++++++ 15 files changed, 144 insertions(+), 43 deletions(-) create mode 100644 changelog.d/7829.bugfix create mode 100644 vector/src/main/res/drawable/ic_voice_broadcast_error.xml diff --git a/changelog.d/7829.bugfix b/changelog.d/7829.bugfix new file mode 100644 index 0000000000..705f7310f0 --- /dev/null +++ b/changelog.d/7829.bugfix @@ -0,0 +1 @@ +Handle exceptions when listening 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 d9f94ba27b..ce84ef61ad 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3121,6 +3121,7 @@ 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. + Unable to play 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 380c80775b..78aaa058e9 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 @@ -157,6 +157,8 @@ 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.UnableToPlay, + is VoiceBroadcastFailure.ListeningError.DownloadError -> stringProvider.getString(R.string.error_voice_broadcast_unable_to_play) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt index a7b926f29a..b5c4b4a537 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/voice/VoiceMessageRecorderView.kt @@ -229,6 +229,7 @@ class VoiceMessageRecorderView @JvmOverloads constructor( voiceMessageViews.renderPlaying(state) } is AudioMessagePlaybackTracker.Listener.State.Paused, + is AudioMessagePlaybackTracker.Listener.State.Error, is AudioMessagePlaybackTracker.Listener.State.Idle -> { voiceMessageViews.renderIdle() } 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 cc3a015120..ad8e163633 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 @@ -15,6 +15,7 @@ */ package im.vector.app.features.home.room.detail.timeline.factory +import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.features.displayname.getBestName @@ -45,6 +46,7 @@ class VoiceBroadcastItemFactory @Inject constructor( private val avatarSizeProvider: AvatarSizeProvider, private val colorProvider: ColorProvider, private val drawableProvider: DrawableProvider, + private val errorFormatter: ErrorFormatter, private val voiceBroadcastRecorder: VoiceBroadcastRecorder?, private val voiceBroadcastPlayer: VoiceBroadcastPlayer, private val playbackTracker: AudioMessagePlaybackTracker, @@ -82,6 +84,7 @@ class VoiceBroadcastItemFactory @Inject constructor( roomItem = session.getRoom(params.event.roomId)?.roomSummary()?.toMatrixItem(), colorProvider = colorProvider, drawableProvider = drawableProvider, + errorFormatter = errorFormatter, ) return if (isRecording) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt index c34cbbc74a..c598a99af7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/AudioMessagePlaybackTracker.kt @@ -50,8 +50,11 @@ class AudioMessagePlaybackTracker @Inject constructor() { listeners.remove(id) } - fun pauseAllPlaybacks() { - listeners.keys.forEach(::pausePlayback) + fun unregisterListeners() { + listeners.forEach { + it.value.onUpdate(Listener.State.Idle) + } + listeners.clear() } /** @@ -84,6 +87,10 @@ class AudioMessagePlaybackTracker @Inject constructor() { } } + fun pauseAllPlaybacks() { + listeners.keys.forEach(::pausePlayback) + } + fun pausePlayback(id: String) { val state = getPlaybackState(id) if (state is Listener.State.Playing) { @@ -94,7 +101,14 @@ class AudioMessagePlaybackTracker @Inject constructor() { } fun stopPlayback(id: String) { - setState(id, Listener.State.Idle) + val state = getPlaybackState(id) + if (state !is Listener.State.Error) { + setState(id, Listener.State.Idle) + } + } + + fun onError(id: String, error: Throwable) { + setState(id, Listener.State.Error(error)) } fun updatePlayingAtPlaybackTime(id: String, time: Int, percentage: Float) { @@ -116,6 +130,7 @@ class AudioMessagePlaybackTracker @Inject constructor() { is Listener.State.Playing -> state.playbackTime is Listener.State.Paused -> state.playbackTime is Listener.State.Recording, + is Listener.State.Error, Listener.State.Idle, null -> null } @@ -126,18 +141,12 @@ class AudioMessagePlaybackTracker @Inject constructor() { is Listener.State.Playing -> state.percentage is Listener.State.Paused -> state.percentage is Listener.State.Recording, + is Listener.State.Error, Listener.State.Idle, null -> null } } - fun unregisterListeners() { - listeners.forEach { - it.value.onUpdate(Listener.State.Idle) - } - listeners.clear() - } - companion object { const val RECORDING_ID = "RECORDING_ID" } @@ -148,6 +157,7 @@ class AudioMessagePlaybackTracker @Inject constructor() { sealed class State { object Idle : State() + data class Error(val failure: Throwable) : State() data class Playing(val playbackTime: Int, val percentage: Float) : State() data class Paused(val playbackTime: Int, val percentage: Float) : State() data class Recording(val amplitudeList: List) : State() 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 c6b90cdabe..7cde978e42 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 @@ -22,6 +22,7 @@ import androidx.annotation.IdRes import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import im.vector.app.R +import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.tintBackground import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider @@ -48,6 +49,7 @@ abstract class AbsMessageVoiceBroadcastItem() { private fun renderStateBasedOnAudioPlayback(holder: Holder) { audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state -> when (state) { + is AudioMessagePlaybackTracker.Listener.State.Error, is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder) is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state) is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt index d21e6771d9..0aa2aaad3b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceBroadcastListeningItem.kt @@ -20,11 +20,13 @@ import android.text.format.DateUtils import android.widget.ImageButton import android.widget.SeekBar import android.widget.TextView +import androidx.constraintlayout.widget.Group import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.core.epoxy.onClick +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.timeline.helper.AudioMessagePlaybackTracker.Listener.State import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer @@ -54,6 +56,16 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } } player.addListener(voiceBroadcast, playerListener) + + playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState -> + renderBackwardForwardButtons(holder, playbackState) + renderPlaybackError(holder, playbackState) + renderLiveIndicator(holder) + if (!isUserSeeking) { + holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0 + } + } + bindSeekBar(holder) bindButtons(holder) } @@ -66,6 +78,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem VoiceBroadcastPlayer.State.Playing, VoiceBroadcastPlayer.State.Buffering -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.Pause) VoiceBroadcastPlayer.State.Paused, + is VoiceBroadcastPlayer.State.Error, VoiceBroadcastPlayer.State.Idle -> callback?.onTimelineItemAction(VoiceBroadcastAction.Listening.PlayOrResume(voiceBroadcast)) } } else { @@ -109,6 +122,7 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem playPauseButton.setImageResource(R.drawable.ic_play_pause_pause) playPauseButton.contentDescription = view.resources.getString(R.string.a11y_pause_voice_broadcast) } + is VoiceBroadcastPlayer.State.Error, VoiceBroadcastPlayer.State.Idle, VoiceBroadcastPlayer.State.Paused -> { playPauseButton.setImageResource(R.drawable.ic_play_pause_play) @@ -120,6 +134,18 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } } + private fun renderPlaybackError(holder: Holder, playbackState: State) { + with(holder) { + if (playbackState is State.Error) { + controlsGroup.isVisible = false + errorView.setTextOrHide(errorFormatter.toHumanReadable(playbackState.failure)) + } else { + errorView.isVisible = false + controlsGroup.isVisible = true + } + } + } + private fun bindSeekBar(holder: Holder) { with(holder) { remainingTimeView.text = formatRemainingTime(duration) @@ -141,13 +167,6 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem } }) } - playbackTracker.track(voiceBroadcast.voiceBroadcastId) { playbackState -> - renderBackwardForwardButtons(holder, playbackState) - renderLiveIndicator(holder) - if (!isUserSeeking) { - holder.seekBar.progress = playbackTracker.getPlaybackTime(voiceBroadcast.voiceBroadcastId) ?: 0 - } - } } private fun renderBackwardForwardButtons(holder: Holder, playbackState: State) { @@ -187,6 +206,8 @@ abstract class MessageVoiceBroadcastListeningItem : AbsMessageVoiceBroadcastItem val broadcasterNameMetadata by bind(R.id.broadcasterNameMetadata) val voiceBroadcastMetadata by bind(R.id.voiceBroadcastMetadata) val listenersCountMetadata by bind(R.id.listenersCountMetadata) + val errorView by bind(R.id.errorView) + val controlsGroup by bind(R.id.controlsGroup) } companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt index d3f320db7d..a8e215b4a9 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageVoiceItem.kt @@ -124,6 +124,7 @@ abstract class MessageVoiceItem : AbsMessageItem() { audioMessagePlaybackTracker.track(attributes.informationData.eventId) { state -> when (state) { + is AudioMessagePlaybackTracker.Listener.State.Error, is AudioMessagePlaybackTracker.Listener.State.Idle -> renderIdleState(holder, waveformColorIdle, waveformColorPlayed) is AudioMessagePlaybackTracker.Listener.State.Playing -> renderPlayingState(holder, state, waveformColorIdle, waveformColorPlayed) is AudioMessagePlaybackTracker.Listener.State.Paused -> renderPausedState(holder, state, waveformColorIdle, waveformColorPlayed) 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 index 76b50c78ab..75863dc042 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/VoiceBroadcastFailure.kt @@ -16,10 +16,21 @@ package im.vector.app.features.voicebroadcast +import android.media.MediaPlayer + sealed class VoiceBroadcastFailure : Throwable() { sealed class RecordingError : VoiceBroadcastFailure() { object NoPermission : RecordingError() object BlockedBySomeoneElse : RecordingError() object UserAlreadyBroadcasting : RecordingError() } + + sealed class ListeningError : VoiceBroadcastFailure() { + /** + * @property what the type of error that has occurred, see [MediaPlayer.OnErrorListener.onError]. + * @property extra an extra code, specific to the error, see [MediaPlayer.OnErrorListener.onError]. + */ + data class UnableToPlay(val what: Int, val extra: Int) : ListeningError() + data class DownloadError(override val cause: Throwable?) : ListeningError() + } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt index 5e5c320d43..ad0ecf69b4 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayer.kt @@ -16,6 +16,7 @@ package im.vector.app.features.voicebroadcast.listening +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.model.VoiceBroadcast interface VoiceBroadcastPlayer { @@ -72,6 +73,7 @@ interface VoiceBroadcastPlayer { object Playing : State object Paused : State object Buffering : State + data class Error(val failure: VoiceBroadcastFailure.ListeningError) : State object Idle : State } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index f00d657682..538b2f8da4 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -24,7 +24,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.onFirst import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.session.coroutineScope -import im.vector.app.features.voice.VoiceFailure +import im.vector.app.features.voicebroadcast.VoiceBroadcastFailure import im.vector.app.features.voicebroadcast.isLive import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.Listener import im.vector.app.features.voicebroadcast.listening.VoiceBroadcastPlayer.State @@ -193,6 +193,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( startPlayback(nextItem.startTime) } } + is State.Error -> Unit State.Idle -> Unit // Should not happen } } @@ -205,20 +206,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val sequence = playlistItem.sequence ?: run { Timber.w("## Voice Broadcast | Playlist item has no sequence"); return } val sequencePosition = position - playlistItem.startTime sessionScope.launch { - try { - prepareMediaPlayer(content) { mp -> - currentMediaPlayer = mp - playlist.currentSequence = sequence - mp.start() - if (sequencePosition > 0) { - mp.seekTo(sequencePosition) - } - playingState = State.Playing - prepareNextMediaPlayer() + prepareMediaPlayer(content) { mp -> + currentMediaPlayer = mp + playlist.currentSequence = sequence + mp.start() + if (sequencePosition > 0) { + mp.seekTo(sequencePosition) } - } catch (failure: Throwable) { - Timber.e(failure, "## Voice Broadcast | Unable to start playback: $failure") - throw VoiceFailure.UnableToPlay(failure) + playingState = State.Playing + prepareNextMediaPlayer() } } } @@ -275,6 +271,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( mp.start() onNextMediaPlayerStarted(mp) } + is State.Error, State.Idle -> stopPlayer() } } @@ -288,11 +285,12 @@ class VoiceBroadcastPlayerImpl @Inject constructor( session.fileService().downloadFile(messageAudioContent) } catch (failure: Throwable) { Timber.e(failure, "Voice Broadcast | Download has failed: $failure") - throw VoiceFailure.UnableToPlay(failure) + throw VoiceBroadcastFailure.ListeningError.DownloadError(failure) } return audioFile.inputStream().use { fis -> MediaPlayer().apply { + setOnErrorListener(mediaPlayerListener) setAudioAttributes( AudioAttributes.Builder() // Do not use CONTENT_TYPE_SPEECH / USAGE_VOICE_COMMUNICATION because we want to play loud here @@ -302,10 +300,9 @@ class VoiceBroadcastPlayerImpl @Inject constructor( ) setDataSource(fis.fd) setOnInfoListener(mediaPlayerListener) - setOnErrorListener(mediaPlayerListener) setOnPreparedListener(onPreparedListener) setOnCompletionListener(mediaPlayerListener) - prepare() + prepareAsync() } } } @@ -330,8 +327,15 @@ class VoiceBroadcastPlayerImpl @Inject constructor( State.Playing -> playbackTicker.startPlaybackTicker(voiceBroadcastId) State.Paused, State.Buffering, + is State.Error, State.Idle -> playbackTicker.stopPlaybackTicker(voiceBroadcastId) } + + // Notify playback tracker about error + if (playingState is State.Error) { + playbackTracker.onError(voiceBroadcastId, playingState.failure) + } + // Notify state change to all the listeners attached to the current voice broadcast id listeners[voiceBroadcastId]?.forEach { listener -> listener.onPlayingStateChanged(playingState) } } @@ -374,7 +378,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun onLiveListeningChanged(isLiveListening: Boolean) { // Live has ended and last chunk has been reached, we can stop the playback - if (!isLiveListening && playingState == State.Buffering && playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence) { + val hasReachedLastChunk = playlist.currentSequence == mostRecentVoiceBroadcastEvent?.content?.lastChunkSequence + if (!isLiveListening && playingState == State.Buffering && hasReachedLastChunk) { stop() } } @@ -389,16 +394,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( private fun getCurrentPlaybackPosition(): Int? { val voiceBroadcastId = currentVoiceBroadcast?.voiceBroadcastId ?: return null - val computedPosition = currentMediaPlayer?.currentPosition?.let { playlist.currentItem?.startTime?.plus(it) } + val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlist.currentItem?.startTime?.plus(it) } val savedPosition = playbackTracker.getPlaybackTime(voiceBroadcastId) return computedPosition ?: savedPosition } private fun getCurrentPlaybackPercentage(): Float? { val playlistPosition = playlist.currentItem?.startTime - val computedPosition = currentMediaPlayer?.currentPosition?.let { playlistPosition?.plus(it) } ?: playlistPosition - val duration = playlist.duration.takeIf { it > 0 } - val computedPercentage = if (computedPosition != null && duration != null) computedPosition.toFloat() / duration else null + val computedPosition = tryOrNull { currentMediaPlayer?.currentPosition }?.let { playlistPosition?.plus(it) } ?: playlistPosition + val duration = playlist.duration + val computedPercentage = if (computedPosition != null && duration > 0) computedPosition.toFloat() / duration else null val savedPercentage = currentVoiceBroadcast?.voiceBroadcastId?.let { playbackTracker.getPercentage(it) } return computedPercentage ?: savedPercentage } @@ -416,6 +421,10 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } override fun onCompletion(mp: MediaPlayer) { + // Release media player as soon as it completed + mp.release() + currentMediaPlayer = null + // Next media player is already attached to this player and will start playing automatically if (nextMediaPlayer != null) return @@ -426,15 +435,16 @@ class VoiceBroadcastPlayerImpl @Inject constructor( // We'll not receive new chunks anymore so we can stop the live listening stop() } else { - // Enter in buffering mode and release current media player playingState = State.Buffering - currentMediaPlayer?.release() - currentMediaPlayer = null + prepareNextMediaPlayer() } } override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean { - stop() + Timber.d("## Voice Broadcast | onError: what=$what, extra=$extra") + if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) { + playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToPlay(what, extra)) + } return true } } @@ -480,6 +490,7 @@ class VoiceBroadcastPlayerImpl @Inject constructor( playbackTracker.updatePausedAtPlaybackTime(id, playbackTime, percentage) } } + is State.Error -> Unit } } } diff --git a/vector/src/main/res/drawable/ic_voice_broadcast_error.xml b/vector/src/main/res/drawable/ic_voice_broadcast_error.xml new file mode 100644 index 0000000000..6cbd4592cb --- /dev/null +++ b/vector/src/main/res/drawable/ic_voice_broadcast_error.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml index 760293ee64..deec85e2ed 100644 --- a/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_voice_broadcast_listening_stub.xml @@ -176,4 +176,27 @@ tools:ignore="NegativeMargin" tools:text="-0:12" /> + + + + From 3663f225905aabdfaa73550224cf210d6616fa96 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 10 Jan 2023 15:16:25 +0100 Subject: [PATCH 03/75] Handle download error during playback --- .../listening/VoiceBroadcastPlayerImpl.kt | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 538b2f8da4..ab4b6c2269 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -206,15 +206,19 @@ class VoiceBroadcastPlayerImpl @Inject constructor( val sequence = playlistItem.sequence ?: run { Timber.w("## Voice Broadcast | Playlist item has no sequence"); return } val sequencePosition = position - playlistItem.startTime sessionScope.launch { - prepareMediaPlayer(content) { mp -> - currentMediaPlayer = mp - playlist.currentSequence = sequence - mp.start() - if (sequencePosition > 0) { - mp.seekTo(sequencePosition) + try { + prepareMediaPlayer(content) { mp -> + currentMediaPlayer = mp + playlist.currentSequence = sequence + mp.start() + if (sequencePosition > 0) { + mp.seekTo(sequencePosition) + } + playingState = State.Playing + prepareNextMediaPlayer() } - playingState = State.Playing - prepareNextMediaPlayer() + } catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) { + playingState = State.Error(failure) } } } @@ -259,20 +263,27 @@ class VoiceBroadcastPlayerImpl @Inject constructor( if (nextItem != null) { isPreparingNextPlayer = true sessionScope.launch { - prepareMediaPlayer(nextItem.audioEvent.content) { mp -> + try { + prepareMediaPlayer(nextItem.audioEvent.content) { mp -> + isPreparingNextPlayer = false + nextMediaPlayer = mp + when (playingState) { + State.Playing, + State.Paused -> { + currentMediaPlayer?.setNextMediaPlayer(mp) + } + State.Buffering -> { + mp.start() + onNextMediaPlayerStarted(mp) + } + is State.Error, + State.Idle -> stopPlayer() + } + } + } catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) { isPreparingNextPlayer = false - nextMediaPlayer = mp - when (playingState) { - State.Playing, - State.Paused -> { - currentMediaPlayer?.setNextMediaPlayer(mp) - } - State.Buffering -> { - mp.start() - onNextMediaPlayerStarted(mp) - } - is State.Error, - State.Idle -> stopPlayer() + if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) { + playingState = State.Error(failure) } } } From 9d3b5c5bbb7c0d2a457a11fa6a20214b3e788293 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 10 Jan 2023 16:00:19 +0100 Subject: [PATCH 04/75] Fix no display name for some voice broadcast recorder name --- .../room/detail/timeline/factory/VoiceBroadcastItemFactory.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 ad8e163633..3439fb1f57 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 @@ -18,7 +18,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider -import im.vector.app.features.displayname.getBestName import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.VoiceBroadcastEventsGroup @@ -37,7 +36,6 @@ import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.recording.VoiceBroadcastRecorder import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.getUserOrDefault import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -77,7 +75,7 @@ class VoiceBroadcastItemFactory @Inject constructor( voiceBroadcast = voiceBroadcast, voiceBroadcastState = voiceBroadcastContent.voiceBroadcastState, duration = voiceBroadcastEventsGroup.getDuration(), - recorderName = params.event.root.stateKey?.let { session.getUserOrDefault(it) }?.toMatrixItem()?.getBestName().orEmpty(), + recorderName = params.event.senderInfo.disambiguatedDisplayName, recorder = voiceBroadcastRecorder, player = voiceBroadcastPlayer, playbackTracker = playbackTracker, From 48b54b402b55e9783b696f647815e47a99685d41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:27:20 +0000 Subject: [PATCH 05/75] Bump androidxTest from 1.4.0 to 1.5.0 Bumps `androidxTest` from 1.4.0 to 1.5.0. Updates `core` from 1.4.0 to 1.5.0 Updates `runner` from 1.4.0 to 1.5.0 Updates `rules` from 1.4.0 to 1.5.0 --- updated-dependencies: - dependency-name: androidx.test:core dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: androidx.test:runner dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: androidx.test:rules dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 25785e984e..060391781a 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -32,7 +32,7 @@ def fragment = "1.5.5" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.4.0" -def androidxTest = "1.4.0" +def androidxTest = "1.5.0" def androidxOrchestrator = "1.4.2" def paparazzi = "1.1.0" From 2614911631bd25fbc913d0031a6cdde286ff1a26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Dec 2022 23:03:51 +0000 Subject: [PATCH 06/75] Bump kotlin-reflect from 1.7.22 to 1.8.0 Bumps [kotlin-reflect](https://github.com/JetBrains/kotlin) from 1.7.22 to 1.8.0. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.22...v1.8.0) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-reflect dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- vector-app/build.gradle | 2 +- vector/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector-app/build.gradle b/vector-app/build.gradle index 11119a75cc..a4787e90db 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -403,7 +403,7 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0" debugImplementation libs.androidx.fragmentTesting debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } diff --git a/vector/build.gradle b/vector/build.gradle index 2224634194..e9497c81a0 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -331,5 +331,5 @@ dependencies { androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator debugImplementation libs.androidx.fragmentTesting - androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.22" + androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0" } From 136282d5b1eaca90fe9538c799aec443693dd500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 28 Dec 2022 23:08:38 +0000 Subject: [PATCH 07/75] Bump kotlin-gradle-plugin from 1.7.22 to 1.8.0 Bumps [kotlin-gradle-plugin](https://github.com/JetBrains/kotlin) from 1.7.22 to 1.8.0. - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.22...v1.8.0) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 060391781a..c5dc908098 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -8,7 +8,7 @@ ext.versions = [ def gradle = "7.3.1" // Ref: https://kotlinlang.org/releases.html -def kotlin = "1.7.22" +def kotlin = "1.8.0" def kotlinCoroutines = "1.6.4" def dagger = "2.44.2" def firebaseBom = "31.1.1" From 6c50c75220823b5457639b54422c4726c72278fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Jan 2023 09:57:59 +0000 Subject: [PATCH 08/75] Bump espresso from 3.4.0 to 3.5.1 Bumps `espresso` from 3.4.0 to 3.5.1. Updates `espresso-core` from 3.4.0 to 3.5.1 Updates `espresso-contrib` from 3.4.0 to 3.5.1 Updates `espresso-intents` from 3.4.0 to 3.5.1 --- updated-dependencies: - dependency-name: androidx.test.espresso:espresso-core dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: androidx.test.espresso:espresso-contrib dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: androidx.test.espresso:espresso-intents dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index c5dc908098..a9a1b24cc5 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -31,7 +31,7 @@ def sentry = "6.11.0" def fragment = "1.5.5" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 -def espresso = "3.4.0" +def espresso = "3.5.1" def androidxTest = "1.5.0" def androidxOrchestrator = "1.4.2" def paparazzi = "1.1.0" From e502d5d9a6a0f126d26c962805927496651d9e22 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Dec 2022 23:04:09 +0000 Subject: [PATCH 09/75] Bump com.google.devtools.ksp from 1.7.22-1.0.8 to 1.8.0-1.0.8 Bumps [com.google.devtools.ksp](https://github.com/google/ksp) from 1.7.22-1.0.8 to 1.8.0-1.0.8. - [Release notes](https://github.com/google/ksp/releases) - [Commits](https://github.com/google/ksp/compare/1.7.22-1.0.8...1.8.0-1.0.8) --- updated-dependencies: - dependency-name: com.google.devtools.ksp dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 53b7a983ec..850a4143c9 100644 --- a/build.gradle +++ b/build.gradle @@ -45,7 +45,7 @@ plugins { // Detekt id "io.gitlab.arturbosch.detekt" version "1.22.0" // Ksp - id "com.google.devtools.ksp" version "1.7.22-1.0.8" + id "com.google.devtools.ksp" version "1.8.0-1.0.8" // Dependency Analysis id 'com.autonomousapps.dependency-analysis' version "1.18.0" From d686d7aab24fd9a879b082962d6429c49546f89a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jan 2023 11:22:46 +0100 Subject: [PATCH 10/75] Project property value can only be strings. Fix > Failed to notify project evaluation listener. > Could not create task ':element-android:matrix-sdk-android:compileDebugAndroidTestKotlin'. > Could not create task of type 'KotlinCompile'. > class java.util.LinkedHashMap cannot be cast to class java.lang.String (java.util.LinkedHashMap and java.lang.String are in module java.base of loader 'bootstrap') --- coverage.gradle | 4 ++-- matrix-sdk-android/build.gradle | 2 +- vector-app/build.gradle | 2 +- vector/build.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coverage.gradle b/coverage.gradle index 2c0af25368..421c500728 100644 --- a/coverage.gradle +++ b/coverage.gradle @@ -80,12 +80,12 @@ task generateCoverageReport(type: JacocoReport) { task unitTestsWithCoverage(type: GradleBuild) { // the 7.1.3 android gradle plugin has a bug where enableTestCoverage generates invalid coverage - startParameter.projectProperties.coverage = [enableTestCoverage: false] + startParameter.projectProperties.coverage = "false" tasks = ['testDebugUnitTest'] } task instrumentationTestsWithCoverage(type: GradleBuild) { - startParameter.projectProperties.coverage = [enableTestCoverage: true] + startParameter.projectProperties.coverage = "true" startParameter.projectProperties['android.testInstrumentationRunnerArguments.notPackage'] = 'im.vector.app.ui' tasks = [':vector-app:connectedGplayDebugAndroidTest', ':vector:connectedDebugAndroidTest', 'matrix-sdk-android:connectedDebugAndroidTest'] } diff --git a/matrix-sdk-android/build.gradle b/matrix-sdk-android/build.gradle index ef6cb8cddb..9c9d2dd0dc 100644 --- a/matrix-sdk-android/build.gradle +++ b/matrix-sdk-android/build.gradle @@ -81,7 +81,7 @@ android { buildTypes { debug { if (project.hasProperty("coverage")) { - testCoverageEnabled = coverage.enableTestCoverage + testCoverageEnabled = coverage == "true" } // Set to true to log privacy or sensible data, such as token buildConfigField "boolean", "LOG_PRIVATE_DATA", project.property("vector.debugPrivateData") diff --git a/vector-app/build.gradle b/vector-app/build.gradle index a4787e90db..b2cfb7b426 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -232,7 +232,7 @@ android { resValue "color", "launcher_background", "#0DBD8B" if (project.hasProperty("coverage")) { - testCoverageEnabled = coverage.enableTestCoverage + testCoverageEnabled = coverage == "true" } } diff --git a/vector/build.gradle b/vector/build.gradle index e9497c81a0..c5bb55ae9a 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -69,7 +69,7 @@ android { buildTypes { debug { if (project.hasProperty("coverage")) { - testCoverageEnabled = coverage.enableTestCoverage + testCoverageEnabled = coverage == "true" } } } From 242596744ac1f620cde5d48dffaeea006b4f5906 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jan 2023 12:03:41 +0100 Subject: [PATCH 11/75] Use Fragment 1.6.0 alpha to fix issue with test https://issuetracker.google.com/issues/128612536 --- dependencies.gradle | 4 +++- vector-app/build.gradle | 2 +- vector/build.gradle | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index a9a1b24cc5..8b0933b943 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -28,7 +28,8 @@ def jjwt = "0.11.5" // the whole commit which set version 0.16.0-SNAPSHOT def vanniktechEmoji = "0.16.0-SNAPSHOT" def sentry = "6.11.0" -def fragment = "1.5.5" +// Use 1.6.0 alpha to fix issue with test +def fragment = "1.6.0-alpha04" // Testing def mockk = "1.12.3" // We need to use 1.12.3 to have mocking in androidTest until a new version is released: https://github.com/mockk/mockk/issues/819 def espresso = "3.5.1" @@ -56,6 +57,7 @@ ext.libs = [ 'exifinterface' : "androidx.exifinterface:exifinterface:1.3.5", 'fragmentKtx' : "androidx.fragment:fragment-ktx:$fragment", 'fragmentTesting' : "androidx.fragment:fragment-testing:$fragment", + 'fragmentTestingManifest' : "androidx.fragment:fragment-testing-manifest:$fragment", 'constraintLayout' : "androidx.constraintlayout:constraintlayout:2.1.4", 'work' : "androidx.work:work-runtime-ktx:2.7.1", 'autoFill' : "androidx.autofill:autofill:1.1.0", diff --git a/vector-app/build.gradle b/vector-app/build.gradle index b2cfb7b426..824f651b4d 100644 --- a/vector-app/build.gradle +++ b/vector-app/build.gradle @@ -404,7 +404,7 @@ dependencies { androidTestUtil libs.androidx.orchestrator androidTestImplementation libs.androidx.fragmentTesting androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0" - debugImplementation libs.androidx.fragmentTesting + debugImplementation libs.androidx.fragmentTestingManifest debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' } diff --git a/vector/build.gradle b/vector/build.gradle index c5bb55ae9a..efea312bed 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -330,6 +330,7 @@ dependencies { } androidTestImplementation libs.mockk.mockkAndroid androidTestUtil libs.androidx.orchestrator - debugImplementation libs.androidx.fragmentTesting + debugImplementation libs.androidx.fragmentTestingManifest + androidTestImplementation libs.androidx.fragmentTesting androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.8.0" } From 0c045f3b111659bcb39214d1be151b88001ace41 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jan 2023 12:20:23 +0100 Subject: [PATCH 12/75] Changelog file --- changelog.d/7936.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7936.misc diff --git a/changelog.d/7936.misc b/changelog.d/7936.misc new file mode 100644 index 0000000000..8480d9a6bf --- /dev/null +++ b/changelog.d/7936.misc @@ -0,0 +1 @@ +Upgrade to Kotlin 1.8 From 912d3e5055d301b5d4bd0c143b6ce9e21d5f9954 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 11 Jan 2023 15:57:39 +0300 Subject: [PATCH 13/75] Fix edited poll preview in room list. --- .../room/detail/timeline/format/DisplayableEventFormatter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index 5fa9576dd4..ef4846d822 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -130,7 +130,7 @@ class DisplayableEventFormatter @Inject constructor( span { } } in EventType.POLL_START.values -> { - timelineEvent.root.getClearContent().toModel(catchError = true)?.getBestPollCreationInfo()?.question?.getBestQuestion() + (timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: stringProvider.getString(R.string.sent_a_poll) } in EventType.POLL_RESPONSE.values -> { From 61f7f12d7f40e4e33c937b02ee0a4a7910b3c06f Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 11 Jan 2023 15:58:10 +0300 Subject: [PATCH 14/75] Fix edited rendering poll question in action preview. --- .../room/detail/timeline/action/MessageActionsViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 646cfa50d2..d442c1f1ba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -216,8 +216,8 @@ class MessageActionsViewModel @AssistedInject constructor( noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse()) } in EventType.POLL_START.values -> { - timelineEvent.root.getClearContent().toModel(catchError = true) - ?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "" + (timelineEvent.getVectorLastMessageContent() as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() + ?: stringProvider.getString(R.string.message_reply_to_poll_preview) } else -> null } From 62e0c80a0633bfefde04d58b9c76c3ee2ea1aabf Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 11 Jan 2023 15:59:15 +0300 Subject: [PATCH 15/75] Fix rendering edited poll in timeline. --- .../sdk/api/session/room/timeline/TimelineEvent.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt index 6320ea964d..3aa480094c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineEvent.kt @@ -148,8 +148,8 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? { // Polls/Beacon are not message contents like others as there is no msgtype subtype to discriminate moshi parsing // so toModel won't parse them correctly // It's discriminated on event type instead. Maybe it shouldn't be MessageContent at all to avoid confusion? - in EventType.POLL_START.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel() - in EventType.POLL_END.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel() + in EventType.POLL_START.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel() + in EventType.POLL_END.values -> (getLastPollEditNewContent() ?: root.getClearContent()).toModel() in EventType.STATE_ROOM_BEACON_INFO.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel() in EventType.BEACON_LOCATION_DATA.values -> (getLastEditNewContent() ?: root.getClearContent()).toModel() else -> (getLastEditNewContent() ?: root.getClearContent()).toModel() @@ -160,6 +160,10 @@ fun TimelineEvent.getLastEditNewContent(): Content? { return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent } +private fun TimelineEvent.getLastPollEditNewContent(): Content? { + return annotations?.editSummary?.latestEdit?.getClearContent()?.toModel()?.newContent +} + /** * Returns true if it's a reply. */ From ca99dc8a33c31a912d5e120721f6cc8d9f48e9d3 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 11 Jan 2023 16:10:06 +0300 Subject: [PATCH 16/75] Add changelog. --- changelog.d/7938.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7938.bugfix diff --git a/changelog.d/7938.bugfix b/changelog.d/7938.bugfix new file mode 100644 index 0000000000..70218edf8a --- /dev/null +++ b/changelog.d/7938.bugfix @@ -0,0 +1 @@ +Fix rendering of edited polls From f2d183520d1c43f259e6dbf40dce9aa90173c5ea Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Jan 2023 15:18:28 +0100 Subject: [PATCH 17/75] Add SECURITY.md (copied from https://github.com/vector-im/.github/blob/main/SECURITY.md) --- SECURITY.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..3126b47a07 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Reporting a Vulnerability + +**If you've found a security vulnerability, please report it to security@matrix.org** + +For more information on our security disclosure policy, visit https://www.matrix.org/security-disclosure-policy/ From c63d6fa1fbf8b88ef3cf026072be21cf14c43abe Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 4 Jan 2023 17:25:38 +0100 Subject: [PATCH 18/75] Fix unexpected live voice broadcast in the room list --- .../GetRoomLiveVoiceBroadcastsUseCase.kt | 9 +++- .../GetVoiceBroadcastStateEventLiveUseCase.kt | 13 +---- .../GetVoiceBroadcastStateEventUseCase.kt | 54 +++++++++++++++++++ 3 files changed, 64 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt index fa5f06bfe6..fb48328305 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetRoomLiveVoiceBroadcastsUseCase.kt @@ -19,14 +19,20 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.isLive +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.voiceBroadcastId import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.getRoom import javax.inject.Inject +/** + * Get the list of live (not ended) voice broadcast events in the given room. + */ class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor( private val activeSessionHolder: ActiveSessionHolder, + private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase, ) { fun execute(roomId: String): List { @@ -37,7 +43,8 @@ class GetRoomLiveVoiceBroadcastsUseCase @Inject constructor( setOf(VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO), QueryStringValue.IsNotEmpty ) - .mapNotNull { it.asVoiceBroadcastEvent() } + .mapNotNull { stateEvent -> stateEvent.asVoiceBroadcastEvent()?.voiceBroadcastId } + .mapNotNull { voiceBroadcastId -> getVoiceBroadcastStateEventUseCase.execute(VoiceBroadcast(voiceBroadcastId, roomId)) } .filter { it.isLive } } } diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt index b3bbdad635..22fb0df6f9 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventLiveUseCase.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.transformWhile 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.RelationType import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.util.Optional @@ -44,6 +43,7 @@ import javax.inject.Inject class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor( private val session: Session, + private val getVoiceBroadcastStateEventUseCase: GetVoiceBroadcastStateEventUseCase, ) { fun execute(voiceBroadcast: VoiceBroadcast): Flow> { @@ -93,7 +93,7 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor( * Get a flow of the most recent related event. */ private fun getMostRecentRelatedEventFlow(room: Room, voiceBroadcast: VoiceBroadcast): Flow> { - val mostRecentEvent = getMostRecentRelatedEvent(room, voiceBroadcast).toOptional() + val mostRecentEvent = getVoiceBroadcastStateEventUseCase.execute(voiceBroadcast).toOptional() return if (mostRecentEvent.hasValue()) { val stateKey = mostRecentEvent.get().root.stateKey.orEmpty() // observe incoming voice broadcast state events @@ -141,15 +141,6 @@ class GetVoiceBroadcastStateEventLiveUseCase @Inject constructor( } } - /** - * Get the most recent event related to the given voice broadcast. - */ - private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { - return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) - .mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeUnless { it.root.isRedacted() } } - .maxByOrNull { it.root.originServerTs ?: 0 } - } - /** * Get a flow of the given voice broadcast event changes. */ diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt new file mode 100644 index 0000000000..9c3d1cced3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt @@ -0,0 +1,54 @@ +/* + * 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.usecase + +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.voiceBroadcastId +import org.matrix.android.sdk.api.session.Session +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.Room +import timber.log.Timber +import javax.inject.Inject + +class GetVoiceBroadcastStateEventUseCase @Inject constructor( + private val session: Session, +) { + + fun execute(voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { + val room = session.getRoom(voiceBroadcast.roomId) ?: error("Unknown roomId: ${voiceBroadcast.roomId}") + return getMostRecentRelatedEvent(room, voiceBroadcast) + .also { event -> + Timber.d( + "## VoiceBroadcast | " + + "voiceBroadcastId=${event?.voiceBroadcastId}, " + + "state=${event?.content?.voiceBroadcastState}" + ) + } + } + + /** + * Get the most recent event related to the given voice broadcast. + */ + private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { + return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) + .mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeUnless { it.root.isRedacted() } } + .maxByOrNull { it.root.originServerTs ?: 0 } + } +} From 63dccb4f3bce1f711c0ac107d17a17f8fcc08f18 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 5 Jan 2023 14:32:25 +0100 Subject: [PATCH 19/75] Add changelog file --- changelog.d/7832.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7832.bugfix diff --git a/changelog.d/7832.bugfix b/changelog.d/7832.bugfix new file mode 100644 index 0000000000..871f9aabb9 --- /dev/null +++ b/changelog.d/7832.bugfix @@ -0,0 +1 @@ +[Voice Broadcast] Fix unexpected "live broadcast" in the room list From 39c0cb201504cfc59b9ac45fa4ffed6534dbd64d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 6 Jan 2023 14:58:51 +0100 Subject: [PATCH 20/75] Add unit test --- .../GetVoiceBroadcastStateEventUseCaseTest.kt | 114 ++++++++++++++++++ .../java/im/vector/app/test/fakes/FakeRoom.kt | 2 +- .../vector/app/test/fakes/FakeRoomService.kt | 2 +- 3 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt new file mode 100644 index 0000000000..779ac39273 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2023 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.usecase + +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.model.VoiceBroadcast +import im.vector.app.test.fakes.FakeSession +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull +import org.amshove.kluent.shouldNotBeNull +import org.junit.Test +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +private const val A_ROOM_ID = "A_ROOM_ID" +private const val A_VOICE_BROADCAST_ID = "A_VOICE_BROADCAST_ID" + +internal class GetVoiceBroadcastStateEventUseCaseTest { + + private val fakeSession = FakeSession() + private val getVoiceBroadcastStateEventUseCase = GetVoiceBroadcastStateEventUseCase(fakeSession) + + private val fakeRoom get() = fakeSession.fakeRoomService.fakeRoom + + @Test + fun `given there is no event related to the given vb, when execute, then return null`() { + // Given + val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) + every { fakeRoom.fakeTimelineService.getTimelineEventsRelatedTo(any(), any()) } returns emptyList() + + // When + val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast) + + // Then + result.shouldBeNull() + } + + @Test + fun `given there are several related events related to the given vb, when execute, then return the most recent one`() { + // Given + val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) + val aListOfTimelineEvents = listOf( + mockk(relaxed = true) { + every { root.eventId } returns "event_id_1" + every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { root.isRedacted() } returns false + every { root.originServerTs } returns 1L + }, + mockk(relaxed = true) { + every { root.eventId } returns "event_id_3" + every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { root.isRedacted() } returns false + every { root.originServerTs } returns 3L + }, + mockk(relaxed = true) { + every { root.eventId } returns "event_id_2" + every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { root.isRedacted() } returns false + every { root.originServerTs } returns 2L + }, + ) + every { fakeRoom.fakeTimelineService.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents + + // When + val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast) + + // Then + result.shouldNotBeNull() + result.root.eventId shouldBeEqualTo "event_id_3" + } + + @Test + fun `given there are several related events related to the given vb, when execute, then return the most recent one which is not redacted`() { + // Given + val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) + val aListOfTimelineEvents = listOf( + mockk(relaxed = true) { + every { root.eventId } returns "event_id_1" + every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { root.isRedacted() } returns false + every { root.originServerTs } returns 1L + }, + mockk(relaxed = true) { + every { root.eventId } returns "event_id_2" + every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { root.isRedacted() } returns true + every { root.originServerTs } returns 2L + }, + ) + every { fakeRoom.fakeTimelineService.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents + + // When + val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast) + + // Then + result.shouldNotBeNull() + result.root.eventId shouldBeEqualTo "event_id_1" + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt index 7835c314ef..8397095e35 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.Room class FakeRoom( private val fakeLocationSharingService: FakeLocationSharingService = FakeLocationSharingService(), private val fakeSendService: FakeSendService = FakeSendService(), - private val fakeTimelineService: FakeTimelineService = FakeTimelineService(), + val fakeTimelineService: FakeTimelineService = FakeTimelineService(), private val fakeRelationService: FakeRelationService = FakeRelationService(), private val fakeStateService: FakeStateService = FakeStateService(), ) : Room by mockk() { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt index e957266383..7ba4b8e336 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.model.RoomSummary class FakeRoomService( - private val fakeRoom: FakeRoom = FakeRoom() + val fakeRoom: FakeRoom = FakeRoom() ) : RoomService by mockk() { override fun getRoom(roomId: String) = fakeRoom From 2df94807e02a6e522a96f4f550c78e3bf5e17883 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 9 Jan 2023 18:13:05 +0100 Subject: [PATCH 21/75] Restore private visibility of fake component fields --- .../usecase/GetVoiceBroadcastStateEventUseCaseTest.kt | 9 ++++----- .../src/test/java/im/vector/app/test/fakes/FakeRoom.kt | 2 +- .../java/im/vector/app/test/fakes/FakeRoomService.kt | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt index 779ac39273..8da864c22c 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt @@ -25,6 +25,7 @@ import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeNull import org.amshove.kluent.shouldNotBeNull import org.junit.Test +import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent private const val A_ROOM_ID = "A_ROOM_ID" @@ -35,13 +36,11 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { private val fakeSession = FakeSession() private val getVoiceBroadcastStateEventUseCase = GetVoiceBroadcastStateEventUseCase(fakeSession) - private val fakeRoom get() = fakeSession.fakeRoomService.fakeRoom - @Test fun `given there is no event related to the given vb, when execute, then return null`() { // Given val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) - every { fakeRoom.fakeTimelineService.getTimelineEventsRelatedTo(any(), any()) } returns emptyList() + every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns emptyList() // When val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast) @@ -74,7 +73,7 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { every { root.originServerTs } returns 2L }, ) - every { fakeRoom.fakeTimelineService.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents + every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents // When val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast) @@ -102,7 +101,7 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { every { root.originServerTs } returns 2L }, ) - every { fakeRoom.fakeTimelineService.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents + every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents // When val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt index 8397095e35..7835c314ef 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoom.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.Room class FakeRoom( private val fakeLocationSharingService: FakeLocationSharingService = FakeLocationSharingService(), private val fakeSendService: FakeSendService = FakeSendService(), - val fakeTimelineService: FakeTimelineService = FakeTimelineService(), + private val fakeTimelineService: FakeTimelineService = FakeTimelineService(), private val fakeRelationService: FakeRelationService = FakeRelationService(), private val fakeStateService: FakeStateService = FakeStateService(), ) : Room by mockk() { diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt index 7ba4b8e336..e957266383 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeRoomService.kt @@ -22,7 +22,7 @@ import org.matrix.android.sdk.api.session.room.RoomService import org.matrix.android.sdk.api.session.room.model.RoomSummary class FakeRoomService( - val fakeRoom: FakeRoom = FakeRoom() + private val fakeRoom: FakeRoom = FakeRoom() ) : RoomService by mockk() { override fun getRoom(roomId: String) = fakeRoom From 493fa7a0eba80ffc340a241b757609331a7b0544 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 9 Jan 2023 18:17:29 +0100 Subject: [PATCH 22/75] Use private method to mockk voice broadcast event --- .../GetVoiceBroadcastStateEventUseCaseTest.kt | 50 +++++++------------ 1 file changed, 18 insertions(+), 32 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt index 8da864c22c..ea4777cb13 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt @@ -53,25 +53,10 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { fun `given there are several related events related to the given vb, when execute, then return the most recent one`() { // Given val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) - val aListOfTimelineEvents = listOf( - mockk(relaxed = true) { - every { root.eventId } returns "event_id_1" - every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO - every { root.isRedacted() } returns false - every { root.originServerTs } returns 1L - }, - mockk(relaxed = true) { - every { root.eventId } returns "event_id_3" - every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO - every { root.isRedacted() } returns false - every { root.originServerTs } returns 3L - }, - mockk(relaxed = true) { - every { root.eventId } returns "event_id_2" - every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO - every { root.isRedacted() } returns false - every { root.originServerTs } returns 2L - }, + val aListOfTimelineEvents = listOf( + givenAVoiceBroadcastEvent(eventId = "event_id_1", isRedacted = false, timestamp = 1L), + givenAVoiceBroadcastEvent(eventId = "event_id_3", isRedacted = false, timestamp = 3L), + givenAVoiceBroadcastEvent(eventId = "event_id_2", isRedacted = false, timestamp = 2L), ) every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents @@ -87,19 +72,9 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { fun `given there are several related events related to the given vb, when execute, then return the most recent one which is not redacted`() { // Given val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) - val aListOfTimelineEvents = listOf( - mockk(relaxed = true) { - every { root.eventId } returns "event_id_1" - every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO - every { root.isRedacted() } returns false - every { root.originServerTs } returns 1L - }, - mockk(relaxed = true) { - every { root.eventId } returns "event_id_2" - every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO - every { root.isRedacted() } returns true - every { root.originServerTs } returns 2L - }, + val aListOfTimelineEvents = listOf( + givenAVoiceBroadcastEvent(eventId = "event_id_1", isRedacted = false, timestamp = 1L), + givenAVoiceBroadcastEvent(eventId = "event_id_2", isRedacted = true, timestamp = 2L), ) every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents @@ -110,4 +85,15 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { result.shouldNotBeNull() result.root.eventId shouldBeEqualTo "event_id_1" } + + private fun givenAVoiceBroadcastEvent( + eventId: String, + isRedacted: Boolean, + timestamp: Long, + ) = mockk(relaxed = true) { + every { root.eventId } returns eventId + every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { root.isRedacted() } returns isRedacted + every { root.originServerTs } returns timestamp + } } From f62f661d2bed9700d4a0a4b00b19bfe9c41433d9 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 11 Jan 2023 17:21:34 +0100 Subject: [PATCH 23/75] Room list - Do not show live broadcast if the started event is redacted --- .../home/room/list/RoomSummaryItemFactory.kt | 27 +-- .../GetLatestPreviewableEventUseCase.kt | 72 +++++++ .../GetVoiceBroadcastStateEventUseCase.kt | 14 +- .../GetLatestPreviewableEventUseCaseTest.kt | 196 ++++++++++++++++++ .../GetVoiceBroadcastStateEventUseCaseTest.kt | 48 ++++- 5 files changed, 321 insertions(+), 36 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index a55900a5c4..18c8ea3bde 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -22,41 +22,33 @@ import com.airbnb.mvrx.Loading import im.vector.app.R import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter -import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.RoomListDisplayMode import im.vector.app.features.home.room.detail.timeline.format.DisplayableEventFormatter +import im.vector.app.features.home.room.list.usecase.GetLatestPreviewableEventUseCase import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.voicebroadcast.isLive -import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent -import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence 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.getRoom -import org.matrix.android.sdk.api.session.room.getTimelineEvent import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo -import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomSummaryItemFactory @Inject constructor( - private val sessionHolder: ActiveSessionHolder, private val displayableEventFormatter: DisplayableEventFormatter, private val dateFormatter: VectorDateFormatter, private val stringProvider: StringProvider, private val typingHelper: TypingHelper, private val avatarRenderer: AvatarRenderer, private val errorFormatter: ErrorFormatter, - private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, + private val getLatestPreviewableEventUseCase: GetLatestPreviewableEventUseCase, ) { fun create( @@ -142,7 +134,7 @@ class RoomSummaryItemFactory @Inject constructor( val showSelected = selectedRoomIds.contains(roomSummary.roomId) var latestFormattedEvent: CharSequence = "" var latestEventTime = "" - val latestEvent = roomSummary.getVectorLatestPreviewableEvent() + val latestEvent = getLatestPreviewableEventUseCase.execute(roomSummary.roomId) if (latestEvent != null) { latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect, roomSummary.isDirect.not()) latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST) @@ -150,7 +142,8 @@ class RoomSummaryItemFactory @Inject constructor( val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) // Skip typing while there is a live voice broadcast - .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() }.orEmpty() + .takeUnless { latestEvent?.root?.asVoiceBroadcastEvent()?.isLive.orFalse() } + .orEmpty() return if (subtitle.isBlank() && displayMode == RoomListDisplayMode.FILTERED) { createCenteredRoomSummaryItem(roomSummary, displayMode, showSelected, unreadCount, onClick, onLongClick) @@ -240,14 +233,4 @@ class RoomSummaryItemFactory @Inject constructor( else -> stringProvider.getQuantityString(R.plurals.search_space_multiple_parents, size - 1, directParentNames[0], size - 1) } } - - private fun RoomSummary.getVectorLatestPreviewableEvent(): TimelineEvent? { - val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return latestPreviewableEvent - val liveVoiceBroadcastTimelineEvent = getRoomLiveVoiceBroadcastsUseCase.execute(roomId).lastOrNull() - ?.root?.eventId?.let { room.getTimelineEvent(it) } - return latestPreviewableEvent?.takeIf { it.root.getClearType() == EventType.CALL_INVITE } - ?: liveVoiceBroadcastTimelineEvent - ?: latestPreviewableEvent - ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast - } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt new file mode 100644 index 0000000000..6a50e87562 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCase.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 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.home.room.list.usecase + +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.voicebroadcast.isLive +import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase +import im.vector.app.features.voicebroadcast.voiceBroadcastId +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.getRoom +import org.matrix.android.sdk.api.session.room.Room +import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import javax.inject.Inject + +class GetLatestPreviewableEventUseCase @Inject constructor( + private val sessionHolder: ActiveSessionHolder, + private val getRoomLiveVoiceBroadcastsUseCase: GetRoomLiveVoiceBroadcastsUseCase, +) { + + fun execute(roomId: String): TimelineEvent? { + val room = sessionHolder.getSafeActiveSession()?.getRoom(roomId) ?: return null + val roomSummary = room.roomSummary() ?: return null + return getCallEvent(roomSummary) + ?: getLiveVoiceBroadcastEvent(room) + ?: getDefaultLatestEvent(room, roomSummary) + } + + private fun getCallEvent(roomSummary: RoomSummary): TimelineEvent? { + return roomSummary.latestPreviewableEvent + ?.takeIf { it.root.getClearType() == EventType.CALL_INVITE } + } + + private fun getLiveVoiceBroadcastEvent(room: Room): TimelineEvent? { + return getRoomLiveVoiceBroadcastsUseCase.execute(room.roomId) + .lastOrNull() + ?.voiceBroadcastId + ?.let { room.getTimelineEvent(it) } + } + + private fun getDefaultLatestEvent(room: Room, roomSummary: RoomSummary): TimelineEvent? { + val latestPreviewableEvent = roomSummary.latestPreviewableEvent + + // If the default latest event is a live voice broadcast (paused or resumed), rely to the started event + val liveVoiceBroadcastEventId = latestPreviewableEvent?.root?.asVoiceBroadcastEvent()?.takeIf { it.isLive }?.voiceBroadcastId + if (liveVoiceBroadcastEventId != null) { + return room.getTimelineEvent(liveVoiceBroadcastEventId) + } + + return latestPreviewableEvent + ?.takeUnless { it.root.asMessageAudioEvent()?.isVoiceBroadcast().orFalse() } // Skip voice messages related to voice broadcast + } +} diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt index 9c3d1cced3..e821e09119 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCase.kt @@ -20,10 +20,12 @@ import im.vector.app.features.voicebroadcast.model.VoiceBroadcast import im.vector.app.features.voicebroadcast.model.VoiceBroadcastEvent import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import im.vector.app.features.voicebroadcast.voiceBroadcastId +import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.Session 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.Room +import org.matrix.android.sdk.api.session.room.getTimelineEvent import timber.log.Timber import javax.inject.Inject @@ -47,8 +49,14 @@ class GetVoiceBroadcastStateEventUseCase @Inject constructor( * Get the most recent event related to the given voice broadcast. */ private fun getMostRecentRelatedEvent(room: Room, voiceBroadcast: VoiceBroadcast): VoiceBroadcastEvent? { - return room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) - .mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent()?.takeUnless { it.root.isRedacted() } } - .maxByOrNull { it.root.originServerTs ?: 0 } + val startedEvent = room.getTimelineEvent(voiceBroadcast.voiceBroadcastId) + return if (startedEvent?.root?.isRedacted().orTrue()) { + null + } else { + room.timelineService().getTimelineEventsRelatedTo(RelationType.REFERENCE, voiceBroadcast.voiceBroadcastId) + .mapNotNull { timelineEvent -> timelineEvent.root.asVoiceBroadcastEvent() } + .filterNot { it.root.isRedacted() } + .maxByOrNull { it.root.originServerTs ?: 0 } + } } } diff --git a/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt new file mode 100644 index 0000000000..5d526c783b --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/list/usecase/GetLatestPreviewableEventUseCaseTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2023 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.home.room.list.usecase + +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants +import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants.VOICE_BROADCAST_CHUNK_KEY +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState +import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent +import im.vector.app.features.voicebroadcast.usecase.GetRoomLiveVoiceBroadcastsUseCase +import im.vector.app.test.fakes.FakeActiveSessionHolder +import im.vector.app.test.fakes.FakeRoom +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeNull +import org.junit.Before +import org.junit.Test +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.getRoom +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +private const val A_ROOM_ID = "a-room-id" + +internal class GetLatestPreviewableEventUseCaseTest { + + private val fakeRoom = FakeRoom() + private val fakeSessionHolder = FakeActiveSessionHolder() + private val fakeRoomSummary = mockk() + private val fakeGetRoomLiveVoiceBroadcastsUseCase = mockk() + + private val getLatestPreviewableEventUseCase = GetLatestPreviewableEventUseCase( + fakeSessionHolder.instance, + fakeGetRoomLiveVoiceBroadcastsUseCase, + ) + + @Before + fun setup() { + every { fakeSessionHolder.instance.getSafeActiveSession()?.getRoom(A_ROOM_ID) } returns fakeRoom + every { fakeRoom.roomSummary() } returns fakeRoomSummary + every { fakeRoom.roomId } returns A_ROOM_ID + every { fakeRoom.timelineService().getTimelineEvent(any()) } answers { + mockk(relaxed = true) { + every { eventId } returns firstArg() + } + } + } + + @Test + fun `given the latest event is a call invite and there is a live broadcast, when execute, returns the call event`() { + // Given + val aLatestPreviewableEvent = mockk { + every { root.type } returns EventType.MESSAGE + every { root.getClearType() } returns EventType.CALL_INVITE + } + every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent + every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf( + givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "id1"), + givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "id1"), + ).mapNotNull { it.asVoiceBroadcastEvent() } + + // When + val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID) + + // Then + result shouldBe aLatestPreviewableEvent + } + + @Test + fun `given the latest event is not a call invite and there is a live broadcast, when execute, returns the latest broadcast event`() { + // Given + val aLatestPreviewableEvent = mockk { + every { root.type } returns EventType.MESSAGE + every { root.getClearType() } returns EventType.MESSAGE + } + every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent + every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns listOf( + givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STARTED, "vb_id1"), + givenAVoiceBroadcastEvent("id2", VoiceBroadcastState.RESUMED, "vb_id2"), + ).mapNotNull { it.asVoiceBroadcastEvent() } + + // When + val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID) + + // Then + result?.eventId shouldBeEqualTo "vb_id2" + } + + @Test + fun `given there is no live broadcast, when execute, returns the latest event`() { + // Given + val aLatestPreviewableEvent = mockk { + every { root.type } returns EventType.MESSAGE + every { root.getClearType() } returns EventType.MESSAGE + } + every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent + every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList() + + // When + val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID) + + // Then + result shouldBe aLatestPreviewableEvent + } + + @Test + fun `given there is no live broadcast and the latest event is a vb message, when execute, returns null`() { + // Given + val aLatestPreviewableEvent = mockk { + every { root.type } returns EventType.MESSAGE + every { root.getClearType() } returns EventType.MESSAGE + every { root.getClearContent() } returns mapOf( + MessageContent.MSG_TYPE_JSON_KEY to "m.audio", + VOICE_BROADCAST_CHUNK_KEY to "1", + "body" to "", + ) + } + every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent + every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList() + + // When + val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID) + + // Then + result.shouldBeNull() + } + + @Test + fun `given the latest event is an ended vb, when execute, returns the stopped event`() { + // Given + val aLatestPreviewableEvent = mockk { + every { eventId } returns "id1" + every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.STOPPED, "vb_id1") + } + every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent + every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList() + + // When + val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID) + + // Then + result?.eventId shouldBeEqualTo "id1" + } + + @Test + fun `given the latest event is a resumed vb, when execute, returns the started event`() { + // Given + val aLatestPreviewableEvent = mockk { + every { eventId } returns "id1" + every { root } returns givenAVoiceBroadcastEvent("id1", VoiceBroadcastState.RESUMED, "vb_id1") + } + every { fakeRoomSummary.latestPreviewableEvent } returns aLatestPreviewableEvent + every { fakeGetRoomLiveVoiceBroadcastsUseCase.execute(A_ROOM_ID) } returns emptyList() + + // When + val result = getLatestPreviewableEventUseCase.execute(A_ROOM_ID) + + // Then + result?.eventId shouldBeEqualTo "vb_id1" + } + + private fun givenAVoiceBroadcastEvent( + eventId: String, + state: VoiceBroadcastState, + voiceBroadcastId: String, + ): Event = mockk { + every { this@mockk.eventId } returns eventId + every { getClearType() } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { content } returns mapOf( + "state" to state.value, + "m.relates_to" to mapOf( + "rel_type" to RelationType.REFERENCE, + "event_id" to voiceBroadcastId + ) + ) + } +} diff --git a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt index ea4777cb13..00b04aea81 100644 --- a/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/voicebroadcast/usecase/GetVoiceBroadcastStateEventUseCaseTest.kt @@ -18,6 +18,7 @@ package im.vector.app.features.voicebroadcast.usecase import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.model.VoiceBroadcast +import im.vector.app.features.voicebroadcast.model.VoiceBroadcastState import im.vector.app.test.fakes.FakeSession import io.mockk.every import io.mockk.mockk @@ -40,6 +41,7 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { fun `given there is no event related to the given vb, when execute, then return null`() { // Given val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) + every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEvent(A_VOICE_BROADCAST_ID) } returns null every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns emptyList() // When @@ -54,9 +56,9 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { // Given val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) val aListOfTimelineEvents = listOf( - givenAVoiceBroadcastEvent(eventId = "event_id_1", isRedacted = false, timestamp = 1L), - givenAVoiceBroadcastEvent(eventId = "event_id_3", isRedacted = false, timestamp = 3L), - givenAVoiceBroadcastEvent(eventId = "event_id_2", isRedacted = false, timestamp = 2L), + givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = false, timestamp = 1L), + givenAVoiceBroadcastEvent(eventId = "event_id_3", state = VoiceBroadcastState.STOPPED, isRedacted = false, timestamp = 3L), + givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.PAUSED, isRedacted = false, timestamp = 2L), ) every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents @@ -73,8 +75,8 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { // Given val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) val aListOfTimelineEvents = listOf( - givenAVoiceBroadcastEvent(eventId = "event_id_1", isRedacted = false, timestamp = 1L), - givenAVoiceBroadcastEvent(eventId = "event_id_2", isRedacted = true, timestamp = 2L), + givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = false, timestamp = 1L), + givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.STOPPED, isRedacted = true, timestamp = 2L), ) every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents @@ -83,17 +85,41 @@ internal class GetVoiceBroadcastStateEventUseCaseTest { // Then result.shouldNotBeNull() - result.root.eventId shouldBeEqualTo "event_id_1" + result.root.eventId shouldBeEqualTo A_VOICE_BROADCAST_ID + } + + @Test + fun `given a not ended voice broadcast with a redacted start event, when execute, then return null`() { + // Given + val aVoiceBroadcast = VoiceBroadcast(A_VOICE_BROADCAST_ID, A_ROOM_ID) + val aListOfTimelineEvents = listOf( + givenAVoiceBroadcastEvent(eventId = A_VOICE_BROADCAST_ID, state = VoiceBroadcastState.STARTED, isRedacted = true, timestamp = 1L), + givenAVoiceBroadcastEvent(eventId = "event_id_2", state = VoiceBroadcastState.PAUSED, isRedacted = false, timestamp = 2L), + givenAVoiceBroadcastEvent(eventId = "event_id_3", state = VoiceBroadcastState.RESUMED, isRedacted = false, timestamp = 3L), + ) + every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEventsRelatedTo(any(), any()) } returns aListOfTimelineEvents + + // When + val result = getVoiceBroadcastStateEventUseCase.execute(aVoiceBroadcast) + + // Then + result.shouldBeNull() } private fun givenAVoiceBroadcastEvent( eventId: String, + state: VoiceBroadcastState, isRedacted: Boolean, timestamp: Long, - ) = mockk(relaxed = true) { - every { root.eventId } returns eventId - every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO - every { root.isRedacted() } returns isRedacted - every { root.originServerTs } returns timestamp + ): TimelineEvent { + val timelineEvent = mockk { + every { root.eventId } returns eventId + every { root.type } returns VoiceBroadcastConstants.STATE_ROOM_VOICE_BROADCAST_INFO + every { root.content } returns mapOf("state" to state.value) + every { root.isRedacted() } returns isRedacted + every { root.originServerTs } returns timestamp + } + every { fakeSession.getRoom(A_ROOM_ID)?.timelineService()?.getTimelineEvent(eventId) } returns timelineEvent + return timelineEvent } } From a4a7fa69e81377ee6051983a3067f6ca87ff9591 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jan 2023 23:02:02 +0000 Subject: [PATCH 24/75] Bump appcompat from 1.5.1 to 1.6.0 Bumps appcompat from 1.5.1 to 1.6.0. --- updated-dependencies: - dependency-name: androidx.appcompat:appcompat dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 8b0933b943..23f0be97eb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -50,7 +50,7 @@ ext.libs = [ ], androidx : [ 'activity' : "androidx.activity:activity-ktx:1.6.1", - 'appCompat' : "androidx.appcompat:appcompat:1.5.1", + 'appCompat' : "androidx.appcompat:appcompat:1.6.0", 'biometric' : "androidx.biometric:biometric:1.1.0", 'core' : "androidx.core:core-ktx:1.9.0", 'recyclerview' : "androidx.recyclerview:recyclerview:1.2.1", From 12ad6496c200aec33e3e8cc55b4aafefaf7d35f7 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 20 Dec 2022 09:38:32 +0100 Subject: [PATCH 25/75] Adding changelog entry --- changelog.d/7824.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7824.feature diff --git a/changelog.d/7824.feature b/changelog.d/7824.feature new file mode 100644 index 0000000000..3c8b416571 --- /dev/null +++ b/changelog.d/7824.feature @@ -0,0 +1 @@ +[Poll] Warning message on decryption failure of some events From bd7b1f94960336f1fd176b8bb3481bd80d99f3d4 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 20 Dec 2022 17:17:42 +0100 Subject: [PATCH 26/75] (WIP) Introducing new UnableToDecryptEventEntity --- .../database/EventInsertLiveObserver.kt | 3 + .../UnableToDecryptEventLiveObserver.kt | 83 ++++++++++++++++++ .../PollResponseAggregatedSummaryEntity.kt | 1 + .../model/UnableToDecryptEventEntity.kt | 27 ++++++ .../database/query/EventEntityQueries.kt | 9 +- .../sdk/internal/session/SessionModule.kt | 10 +++ .../UnableToDecryptEventLiveProcessor.kt | 33 +++++++ ...yptedEventRelationsAggregationProcessor.kt | 87 +++++++++++++++++++ .../EventRelationsAggregationProcessor.kt | 6 +- .../poll/DefaultPollAggregationProcessor.kt | 4 + .../EncryptedReferenceAggregationProcessor.kt | 48 ++++++++++ .../factory/PollItemViewStateFactory.kt | 1 + 12 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/UnableToDecryptEventLiveObserver.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UnableToDecryptEventEntity.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index d1ca4f48a6..c276e571fe 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -40,6 +40,9 @@ internal class EventInsertLiveObserver @Inject constructor( private val lock = Mutex() + // TODO should we create a dedicated UnableToDecryptEntity or EncryptedEventEntity? + // and process them into a dedicated observer? + // Create also a new LiveProcessor interface for the new entity? override val query = Monarchy.Query { it.where(EventInsertEntity::class.java).equalTo(EventInsertEntityFields.CAN_BE_PROCESSED, true) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/UnableToDecryptEventLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/UnableToDecryptEventLiveObserver.kt new file mode 100644 index 0000000000..7835a16ad1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/UnableToDecryptEventLiveObserver.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database + +import com.zhuinden.monarchy.Monarchy +import io.realm.RealmConfiguration +import io.realm.RealmResults +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +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.model.UnableToDecryptEventEntity +import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntityFields +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.UnableToDecryptEventLiveProcessor +import timber.log.Timber +import javax.inject.Inject + +internal class UnableToDecryptEventLiveObserver @Inject constructor( + @SessionDatabase realmConfiguration: RealmConfiguration, + private val processors: Set<@JvmSuppressWildcards UnableToDecryptEventLiveProcessor> +) : + RealmLiveEntityObserver(realmConfiguration) { + + private val lock = Mutex() + + override val query = Monarchy.Query { + it.where(UnableToDecryptEventEntity::class.java) + } + + override fun onChange(results: RealmResults) { + observerScope.launch { + lock.withLock { + if (!results.isLoaded || results.isEmpty()) { + return@withLock + } + val copiedEvents = ArrayList(results.size) + Timber.v("UnableToDecryptEventEntity updated with ${results.size} results in db") + results.forEach { + // don't use copy from realm over there + val copiedEvent = UnableToDecryptEventEntity(eventId = it.eventId) + copiedEvents.add(copiedEvent) + } + awaitTransaction(realmConfiguration) { realm -> + Timber.v("##Transaction: There are ${copiedEvents.size} events to process ") + copiedEvents.forEach { utdEvent -> + val eventId = utdEvent.eventId + val event = EventEntity.where(realm, eventId).findFirst() + if (event == null) { + Timber.v("Event $eventId not found") + return@forEach + } + val domainEvent = event.asDomain() + processors.forEach { + it.process(realm, domainEvent) + } + } + realm.where(UnableToDecryptEventEntity::class.java) + .`in`(UnableToDecryptEventEntityFields.EVENT_ID, copiedEvents.map { it.eventId }.toTypedArray()) + .findAll() + .deleteAllFromRealm() + } + processors.forEach { it.onPostProcess() } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt index d759bd3cd9..11d2ed64ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt @@ -36,5 +36,6 @@ internal open class PollResponseAggregatedSummaryEntity( var sourceLocalEchoEvents: RealmList = RealmList() ) : RealmObject() { + // TODO add a list of related eventIds which could not be decrypted companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UnableToDecryptEventEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UnableToDecryptEventEntity.kt new file mode 100644 index 0000000000..6393877d38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UnableToDecryptEventEntity.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.model + +import io.realm.RealmObject + +/** + * This class is used to get notification on new UTD events. Since these events cannot be processed + * in EventInsertEntity, we should introduce a dedicated entity for that. + */ +internal open class UnableToDecryptEventEntity( + var eventId: String = "", +) : RealmObject() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt index 0f1c226044..e18495c924 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -25,6 +25,7 @@ import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventInsertEntity import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntity internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity { val eventEntity = realm.where() @@ -32,11 +33,17 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse .equalTo(EventEntityFields.ROOM_ID, roomId) .findFirst() return if (eventEntity == null) { - val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null + val isEncrypted = type == EventType.ENCRYPTED && decryptionResultJson == null + val canBeProcessed = isEncrypted.not() val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply { this.insertType = insertType } realm.insert(insertEntity) + // TODO check with others if it is the right spot to detect UTD events + if (isEncrypted) { + val utdEventEntity = UnableToDecryptEventEntity(eventId = eventId) + realm.insert(utdEventEntity) + } // copy this event entity and return it realm.copyToRealm(this) } else { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index b9f56cbc9f..d33112a922 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -50,6 +50,7 @@ import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory +import org.matrix.android.sdk.internal.database.UnableToDecryptEventLiveObserver import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.CacheDirectory import org.matrix.android.sdk.internal.di.DeviceId @@ -84,6 +85,7 @@ import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService +import org.matrix.android.sdk.internal.session.room.EncryptedEventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor @@ -346,6 +348,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver + @Binds + @IntoSet + abstract fun bindUnableToDecryptEventObserver(observer: UnableToDecryptEventLiveObserver): SessionLifecycleObserver + @Binds @IntoSet abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver @@ -405,4 +411,8 @@ internal abstract class SessionModule { @Binds abstract fun bindPollAggregationProcessor(processor: DefaultPollAggregationProcessor): PollAggregationProcessor + + @Binds + @IntoSet + abstract fun bindEncryptedEventRelationsAggregationProcessor(processor: EncryptedEventRelationsAggregationProcessor): UnableToDecryptEventLiveProcessor } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt new file mode 100644 index 0000000000..c5fa6dc88e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session + +import io.realm.Realm +import org.matrix.android.sdk.api.session.events.model.Event + +internal interface UnableToDecryptEventLiveProcessor { + + fun process(realm: Realm, event: Event) + + /** + * Called after transaction. + * Maybe you prefer to process the events outside of the realm transaction. + */ + suspend fun onPostProcess() { + // Noop by default + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt new file mode 100644 index 0000000000..2cd05a8e6f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room + +import io.realm.Realm +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.LocalEcho +import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.internal.session.UnableToDecryptEventLiveProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor +import timber.log.Timber +import javax.inject.Inject + +internal class EncryptedEventRelationsAggregationProcessor @Inject constructor( + private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor, +) : UnableToDecryptEventLiveProcessor { + + // TODO add unit tests + override fun process(realm: Realm, event: Event) { + val roomId = event.roomId + if (roomId == null) { + Timber.w("Event has no room id ${event.eventId}") + return + } + + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") + + when (event.getClearType()) { + EventType.ENCRYPTED -> { + val encryptedEventContent = event.content.toModel() + processEncryptedContent( + encryptedEventContent = encryptedEventContent, + realm = realm, + event = event, + roomId = roomId, + isLocalEcho = isLocalEcho, + ) + } + else -> Unit + } + } + + private fun processEncryptedContent( + encryptedEventContent: EncryptedEventContent?, + realm: Realm, + event: Event, + roomId: String, + isLocalEcho: Boolean, + ) { + when (encryptedEventContent?.relatesTo?.type) { + RelationType.REPLACE -> { + Timber.w("## UTD replace in room $roomId for event ${event.eventId}") + } + RelationType.RESPONSE -> { + // can we / should we do we something for UTD response?? + Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + } + RelationType.REFERENCE -> { + // can we / should we do we something for UTD reference?? + Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + encryptedReferenceAggregationProcessor.handle(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } + RelationType.ANNOTATION -> { + // can we / should we do we something for UTD annotation?? + Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + } + else -> Unit + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index be73309837..d03f98158b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -61,6 +61,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber @@ -73,6 +74,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private val sessionManager: SessionManager, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val pollAggregationProcessor: PollAggregationProcessor, + private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor, private val editValidator: EventEditValidator, private val clock: Clock, ) : EventInsertLiveProcessor { @@ -170,11 +172,12 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } + // TODO remove this once other implementation is validated // As for now Live event processors are not receiving UTD events. // They will get an update if the event is decrypted later EventType.ENCRYPTED -> { // Relation type is in clear, it might be possible to do some things? - // Notice that if the event is decrypted later, process be called again + // Notice that if the event is decrypted later, process will be called again val encryptedEventContent = event.content.toModel() when (encryptedEventContent?.relatesTo?.type) { RelationType.REPLACE -> { @@ -189,6 +192,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( RelationType.REFERENCE -> { // can we / should we do we something for UTD reference?? Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + encryptedReferenceAggregationProcessor.handle(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) } RelationType.ANNOTATION -> { // can we / should we do we something for UTD annotation?? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index a424becbd6..301996ebab 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -155,6 +155,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor( ) aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent()) + // TODO check also if eventId is part of UTD list of eventIds, if so remove it + return true } @@ -184,6 +186,8 @@ internal class DefaultPollAggregationProcessor @Inject constructor( ensurePollIsFullyAggregated(roomId, pollEventId) } + // TODO check also if eventId is part of UTD list of eventIds, if so remove it + return true } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt new file mode 100644 index 0000000000..442ce24dbb --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt @@ -0,0 +1,48 @@ +/* + * 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 org.matrix.android.sdk.internal.session.room.aggregation.utd + +import io.realm.Realm +import org.matrix.android.sdk.api.session.events.model.Event +import javax.inject.Inject + +class EncryptedReferenceAggregationProcessor @Inject constructor() { + + // TODO add unit tests + fun handle( + realm: Realm, + event: Event, + roomId: String, + isLocalEcho: Boolean, + relatedEventId: String? + ) { + if(isLocalEcho || relatedEventId.isNullOrEmpty()) return + + handlePollReference(realm = realm, event = event, roomId = roomId, relatedEventId = relatedEventId) + } + + // TODO how to check this is working? + private fun handlePollReference( + realm: Realm, + event: Event, + roomId: String, + relatedEventId: String + ) { + // TODO check if relatedEventId is referencing any existing poll event in DB + // TODO if related to a poll, then add the event id into the list of encryptedRelatedEvents in the summary + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index 13f63e86c4..c885f6f3ff 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -35,6 +35,7 @@ class PollItemViewStateFactory @Inject constructor( pollContent: MessagePollContent, informationData: MessageInformationData, ): PollViewState { + // TODO check for decryption failure error in informationData val pollCreationInfo = pollContent.getBestPollCreationInfo() val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() From ae2639aeb069d8e802c019b2d406f9178956c7c0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 21 Dec 2022 14:10:13 +0100 Subject: [PATCH 27/75] Keep track of related events to a poll which had failed to be decrypted --- .../model/PollResponseAggregatedSummary.kt | 4 +++- .../database/EventInsertLiveObserver.kt | 3 --- ...llResponseAggregatedSummaryEntityMapper.kt | 6 ++++-- .../PollResponseAggregatedSummaryEntity.kt | 5 +++-- .../database/model/SessionRealmModule.kt | 3 ++- ...yptedEventRelationsAggregationProcessor.kt | 2 +- .../EncryptedReferenceAggregationProcessor.kt | 21 ++++++++++++------- .../factory/PollItemViewStateFactory.kt | 2 +- 8 files changed, 28 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt index b16852e47d..e8b4ef6ed6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/PollResponseAggregatedSummary.kt @@ -23,5 +23,7 @@ data class PollResponseAggregatedSummary( val nbOptions: Int = 0, // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) val sourceEvents: List, - val localEchos: List + val localEchos: List, + // list of related event ids which are encrypted due to decryption failure + val encryptedRelatedEventIds: List, ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index c276e571fe..d1ca4f48a6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -40,9 +40,6 @@ internal class EventInsertLiveObserver @Inject constructor( private val lock = Mutex() - // TODO should we create a dedicated UnableToDecryptEntity or EncryptedEventEntity? - // and process them into a dedicated observer? - // Create also a new LiveProcessor interface for the new entity? override val query = Monarchy.Query { it.where(EventInsertEntity::class.java).equalTo(EventInsertEntityFields.CAN_BE_PROCESSED, true) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt index 00998af9bb..808a49b958 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt @@ -30,7 +30,8 @@ internal object PollResponseAggregatedSummaryEntityMapper { closedTime = entity.closedTime, localEchos = entity.sourceLocalEchoEvents.toList(), sourceEvents = entity.sourceEvents.toList(), - nbOptions = entity.nbOptions + nbOptions = entity.nbOptions, + encryptedRelatedEventIds = entity.encryptedRelatedEventIds.toList(), ) } @@ -40,7 +41,8 @@ internal object PollResponseAggregatedSummaryEntityMapper { nbOptions = model.nbOptions, closedTime = model.closedTime, sourceEvents = RealmList().apply { addAll(model.sourceEvents) }, - sourceLocalEchoEvents = RealmList().apply { addAll(model.localEchos) } + sourceLocalEchoEvents = RealmList().apply { addAll(model.localEchos) }, + encryptedRelatedEventIds = RealmList().apply { addAll(model.encryptedRelatedEventIds) }, ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt index 11d2ed64ba..906e329f6f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/PollResponseAggregatedSummaryEntity.kt @@ -33,9 +33,10 @@ internal open class PollResponseAggregatedSummaryEntity( // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) var sourceEvents: RealmList = RealmList(), - var sourceLocalEchoEvents: RealmList = RealmList() + var sourceLocalEchoEvents: RealmList = RealmList(), + // list of related event ids which are encrypted due to decryption failure + var encryptedRelatedEventIds: RealmList = RealmList(), ) : RealmObject() { - // TODO add a list of related eventIds which could not be decrypted companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 0d998e8fe1..79b0dd699c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -72,7 +72,8 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit SpaceParentSummaryEntity::class, UserPresenceEntity::class, ThreadSummaryEntity::class, - ThreadListPageEntity::class + ThreadListPageEntity::class, + UnableToDecryptEventEntity::class, ] ) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt index 2cd05a8e6f..7e9a0a9499 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt @@ -75,7 +75,7 @@ internal class EncryptedEventRelationsAggregationProcessor @Inject constructor( RelationType.REFERENCE -> { // can we / should we do we something for UTD reference?? Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - encryptedReferenceAggregationProcessor.handle(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + encryptedReferenceAggregationProcessor.handle(realm, event, isLocalEcho, encryptedEventContent.relatesTo.eventId) } RelationType.ANNOTATION -> { // can we / should we do we something for UTD annotation?? diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt index 442ce24dbb..fdd3bc80e3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt @@ -18,6 +18,8 @@ package org.matrix.android.sdk.internal.session.room.aggregation.utd import io.realm.Realm import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields import javax.inject.Inject class EncryptedReferenceAggregationProcessor @Inject constructor() { @@ -26,23 +28,28 @@ class EncryptedReferenceAggregationProcessor @Inject constructor() { fun handle( realm: Realm, event: Event, - roomId: String, isLocalEcho: Boolean, relatedEventId: String? ) { - if(isLocalEcho || relatedEventId.isNullOrEmpty()) return + if (isLocalEcho || relatedEventId.isNullOrEmpty()) return - handlePollReference(realm = realm, event = event, roomId = roomId, relatedEventId = relatedEventId) + handlePollReference(realm = realm, event = event, relatedEventId = relatedEventId) } - // TODO how to check this is working? private fun handlePollReference( realm: Realm, event: Event, - roomId: String, relatedEventId: String ) { - // TODO check if relatedEventId is referencing any existing poll event in DB - // TODO if related to a poll, then add the event id into the list of encryptedRelatedEvents in the summary + event.eventId?.let { eventId -> + val existingRelatedPoll = getPollSummaryWithEventId(realm, relatedEventId) + existingRelatedPoll?.encryptedRelatedEventIds?.add(eventId) + } + } + + private fun getPollSummaryWithEventId(realm: Realm, eventId: String): PollResponseAggregatedSummaryEntity? { + return realm.where(PollResponseAggregatedSummaryEntity::class.java) + .containsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, eventId) + .findFirst() } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index c885f6f3ff..db11dff007 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -35,7 +35,7 @@ class PollItemViewStateFactory @Inject constructor( pollContent: MessagePollContent, informationData: MessageInformationData, ): PollViewState { - // TODO check for decryption failure error in informationData + // TODO add new field in ViewState to reflect decryption error of related events val pollCreationInfo = pollContent.getBestPollCreationInfo() val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() From c0c5e208bde46610e615862ca23331d248b86096 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 21 Dec 2022 15:16:47 +0100 Subject: [PATCH 28/75] Remove processing of encrypted events from EventRelationsAggregationProcessor --- .../EventRelationsAggregationProcessor.kt | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index d03f98158b..0734b286b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -23,7 +23,6 @@ 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.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -61,7 +60,6 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor -import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber @@ -74,7 +72,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private val sessionManager: SessionManager, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val pollAggregationProcessor: PollAggregationProcessor, - private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor, private val editValidator: EventEditValidator, private val clock: Clock, ) : EventInsertLiveProcessor { @@ -172,34 +169,6 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } } - // TODO remove this once other implementation is validated - // As for now Live event processors are not receiving UTD events. - // They will get an update if the event is decrypted later - EventType.ENCRYPTED -> { - // Relation type is in clear, it might be possible to do some things? - // Notice that if the event is decrypted later, process will be called again - val encryptedEventContent = event.content.toModel() - when (encryptedEventContent?.relatesTo?.type) { - RelationType.REPLACE -> { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - // A replace! - handleReplace(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } - RelationType.RESPONSE -> { - // can we / should we do we something for UTD response?? - Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - } - RelationType.REFERENCE -> { - // can we / should we do we something for UTD reference?? - Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - encryptedReferenceAggregationProcessor.handle(realm, event, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) - } - RelationType.ANNOTATION -> { - // can we / should we do we something for UTD annotation?? - Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - } - } - } EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } ?: return From 3b9faa5f31f983a8204c4e0ba53e3f7aee63c22a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 21 Dec 2022 15:17:35 +0100 Subject: [PATCH 29/75] Render specific message on decryption error --- .../src/main/res/values/strings.xml | 1 + ...yptedEventRelationsAggregationProcessor.kt | 7 +++++- .../factory/PollItemViewStateFactory.kt | 22 +++++++++++++++---- .../timeline/item/MessageInformationData.kt | 3 ++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f1c2eb2c6d..d884938170 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3194,6 +3194,7 @@ Closed poll Results are only revealed when you end the poll Ended the poll. + Due to decryption errors, some votes may not be counted Active polls There are no active polls in this room Past polls diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt index 7e9a0a9499..bae95f1c15 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt @@ -75,7 +75,12 @@ internal class EncryptedEventRelationsAggregationProcessor @Inject constructor( RelationType.REFERENCE -> { // can we / should we do we something for UTD reference?? Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - encryptedReferenceAggregationProcessor.handle(realm, event, isLocalEcho, encryptedEventContent.relatesTo.eventId) + encryptedReferenceAggregationProcessor.handle( + realm = realm, + event = event, + isLocalEcho = isLocalEcho, + relatedEventId = encryptedEventContent.relatesTo.eventId + ) } RelationType.ANNOTATION -> { // can we / should we do we something for UTD annotation?? diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index db11dff007..64f529fb59 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -31,11 +31,11 @@ class PollItemViewStateFactory @Inject constructor( private val stringProvider: StringProvider, ) { + // TODO update unit tests fun create( pollContent: MessagePollContent, informationData: MessageInformationData, ): PollViewState { - // TODO add new field in ViewState to reflect decryption error of related events val pollCreationInfo = pollContent.getBestPollCreationInfo() val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() @@ -84,9 +84,14 @@ class PollItemViewStateFactory @Inject constructor( totalVotes: Int, winnerVoteCount: Int?, ): PollViewState { + val totalVotesText = if (pollResponseSummary?.hasDecryptionError.orFalse()) { + stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll) + } else { + stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes) + } return PollViewState( question = question, - votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes), + votesStatus = totalVotesText, canVote = false, optionViewStates = pollCreationInfo?.answers?.map { answer -> val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") @@ -127,9 +132,14 @@ class PollItemViewStateFactory @Inject constructor( pollResponseSummary: PollResponseData?, totalVotes: Int ): PollViewState { + val totalVotesText = if (pollResponseSummary?.hasDecryptionError.orFalse()) { + stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll) + } else { + stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes) + } return PollViewState( question = question, - votesStatus = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes), + votesStatus = totalVotesText, canVote = true, optionViewStates = pollCreationInfo?.answers?.map { answer -> val isMyVote = pollResponseSummary?.myVote == answer.id @@ -145,7 +155,11 @@ class PollItemViewStateFactory @Inject constructor( ) } - private fun createReadyPollViewState(question: String, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState { + private fun createReadyPollViewState( + question: String, + pollCreationInfo: PollCreationInfo?, + totalVotes: Int + ): PollViewState { val totalVotesText = if (totalVotes == 0) { stringProvider.getString(R.string.poll_no_votes_cast) } else { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 757246d4e4..4bdfb948ce 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -90,7 +90,8 @@ data class PollResponseData( val votes: Map?, val totalVotes: Int = 0, val winnerVoteCount: Int = 0, - val isClosed: Boolean = false + val isClosed: Boolean = false, + val hasDecryptionError: Boolean = false, ) : Parcelable { fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId) From a29d4399a5b0945cff50c7a53b8ad1210b39a3bd Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 21 Dec 2022 16:34:54 +0100 Subject: [PATCH 30/75] Removing encrypted related id when receiving decrypted event --- .../poll/DefaultPollAggregationProcessor.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 301996ebab..5b25238677 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -45,6 +45,7 @@ import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollRespo import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject +// TODO update unit tests internal class DefaultPollAggregationProcessor @Inject constructor( private val taskExecutor: TaskExecutor, private val fetchPollResponseEventsTask: FetchPollResponseEventsTask, @@ -155,7 +156,7 @@ internal class DefaultPollAggregationProcessor @Inject constructor( ) aggregatedPollSummaryEntity.aggregatedContent = ContentMapper.map(newSumModel.toContent()) - // TODO check also if eventId is part of UTD list of eventIds, if so remove it + event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) } return true } @@ -182,12 +183,12 @@ internal class DefaultPollAggregationProcessor @Inject constructor( aggregatedPollSummaryEntity.sourceEvents.add(event.eventId) } + event.eventId?.let { removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity, it) } + if (!isLocalEcho) { ensurePollIsFullyAggregated(roomId, pollEventId) } - // TODO check also if eventId is part of UTD list of eventIds, if so remove it - return true } @@ -230,4 +231,10 @@ internal class DefaultPollAggregationProcessor @Inject constructor( fetchPollResponseEventsTask.execute(params) } } + + private fun removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity: PollResponseAggregatedSummaryEntity, eventId: String) { + if(aggregatedPollSummaryEntity.encryptedRelatedEventIds.contains(eventId)) { + aggregatedPollSummaryEntity.encryptedRelatedEventIds.remove(eventId) + } + } } From 7e1016da7e42f5dff6c4dca955ecc945a66d3d6d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Wed, 21 Dec 2022 16:35:27 +0100 Subject: [PATCH 31/75] Changing where we insert UnableToDecryptEventEntity in DB --- .../sdk/internal/crypto/CryptoModule.kt | 5 ++ .../sdk/internal/crypto/EventDecryptor.kt | 14 +++++- .../CreateUnableToDecryptEventEntityTask.kt | 46 +++++++++++++++++++ .../database/query/EventEntityQueries.kt | 9 +--- 4 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt index c69a859016..d78f4a3107 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -62,7 +62,9 @@ import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask +import org.matrix.android.sdk.internal.crypto.tasks.CreateUnableToDecryptEventEntityTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice +import org.matrix.android.sdk.internal.crypto.tasks.DefaultCreateUnableToDecryptEventEntityTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask @@ -253,4 +255,7 @@ internal abstract class CryptoModule { @Binds abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask + + @Binds + abstract fun bindCreateUnableToDecryptEventEntityTask(task: DefaultCreateUnableToDecryptEventEntityTask): CreateUnableToDecryptEventEntityTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index c9eabeab48..524db32670 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -38,6 +38,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.tasks.CreateUnableToDecryptEventEntityTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope @@ -59,7 +60,8 @@ internal class EventDecryptor @Inject constructor( private val sendToDeviceTask: SendToDeviceTask, private val deviceListManager: DeviceListManager, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, - private val cryptoStore: IMXCryptoStore + private val cryptoStore: IMXCryptoStore, + private val createUnableToDecryptEventEntityTask: CreateUnableToDecryptEventEntityTask, ) { /** @@ -136,6 +138,7 @@ internal class EventDecryptor @Inject constructor( val eventContent = event.content if (eventContent == null) { Timber.tag(loggerTag.value).e("decryptEvent : empty event content") + createUnableToDecryptEventEntity(event.eventId) throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) } else if (event.isRedacted()) { // we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm @@ -153,6 +156,7 @@ internal class EventDecryptor @Inject constructor( if (alg == null) { val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) Timber.tag(loggerTag.value).e("decryptEvent() : $reason") + createUnableToDecryptEventEntity(event.eventId) throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) } else { try { @@ -171,12 +175,20 @@ internal class EventDecryptor @Inject constructor( } } } + createUnableToDecryptEventEntity(event.eventId) throw mxCryptoError } } } } + private suspend fun createUnableToDecryptEventEntity(eventId: String?) { + eventId?.let { + val params = CreateUnableToDecryptEventEntityTask.Params(eventId = it) + createUnableToDecryptEventEntityTask.execute(params) + } + } + private suspend fun markOlmSessionForUnwedging(senderId: String, senderKey: String) { wedgedMutex.withLock { val info = WedgedDeviceInfo(senderId, senderKey) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt new file mode 100644 index 0000000000..17c7ae5ccd --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.crypto.tasks + +import io.realm.RealmConfiguration +import org.matrix.android.sdk.internal.crypto.store.db.doRealmTransactionAsync +import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntity +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +/** + * This task create a dedicated entity for UTD events so that it can be processed later. + */ +internal interface CreateUnableToDecryptEventEntityTask : Task { + data class Params( + val eventId: String, + ) +} + +// TODO add unit tests +internal class DefaultCreateUnableToDecryptEventEntityTask @Inject constructor( + @SessionDatabase val realmConfiguration: RealmConfiguration, +) : CreateUnableToDecryptEventEntityTask { + + override suspend fun execute(params: CreateUnableToDecryptEventEntityTask.Params) { + val utdEventEntity = UnableToDecryptEventEntity(eventId = params.eventId) + doRealmTransactionAsync(realmConfiguration) { realm -> + realm.insert(utdEventEntity) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt index e18495c924..0f1c226044 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -25,7 +25,6 @@ import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventInsertEntity import org.matrix.android.sdk.internal.database.model.EventInsertType -import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntity internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInsertType): EventEntity { val eventEntity = realm.where() @@ -33,17 +32,11 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse .equalTo(EventEntityFields.ROOM_ID, roomId) .findFirst() return if (eventEntity == null) { - val isEncrypted = type == EventType.ENCRYPTED && decryptionResultJson == null - val canBeProcessed = isEncrypted.not() + val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply { this.insertType = insertType } realm.insert(insertEntity) - // TODO check with others if it is the right spot to detect UTD events - if (isEncrypted) { - val utdEventEntity = UnableToDecryptEventEntity(eventId = eventId) - realm.insert(utdEventEntity) - } // copy this event entity and return it realm.copyToRealm(this) } else { From da6b41c34d94052d8975f6debfb987b4bc27ec46 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 22 Dec 2022 13:31:43 +0100 Subject: [PATCH 32/75] Updating unit tests for PollItemViewStateFactory --- .../factory/PollItemViewStateFactory.kt | 1 - .../factory/PollItemViewStateFactoryTest.kt | 46 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index 64f529fb59..28c560e161 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -31,7 +31,6 @@ class PollItemViewStateFactory @Inject constructor( private val stringProvider: StringProvider, ) { - // TODO update unit tests fun create( pollContent: MessagePollContent, informationData: MessageInformationData, diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 78e544f79d..4d14458e7f 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -131,6 +131,24 @@ class PollItemViewStateFactoryTest { ) } + @Test + fun `given a sent poll state with some decryption error when poll is closed then warning message is displayed`() { + // Given + val stringProvider = FakeStringProvider() + val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasDecryptionError = true) + val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) + + // When + val pollViewState = pollItemViewStateFactory.create( + pollContent = A_POLL_CONTENT, + informationData = closedPollInformationData, + ) + + // Then + pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) + } + @Test fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() { val stringProvider = FakeStringProvider() @@ -193,6 +211,34 @@ class PollItemViewStateFactoryTest { ) } + @Test + fun `given a sent poll with decryption failure when my vote exists then a warning message is displayed`() { + // Given + val stringProvider = FakeStringProvider() + val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) + val votedPollData = A_POLL_RESPONSE_DATA.copy( + totalVotes = 1, + myVote = A_POLL_OPTION_IDS[0], + votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)), + hasDecryptionError = true, + ) + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE + ), + ) + val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData) + + // When + val pollViewState = pollItemViewStateFactory.create( + pollContent = disclosedPollContent, + informationData = votedInformationData, + ) + + // Then + pollViewState.votesStatus shouldBeEqualTo stringProvider.instance.getString(R.string.unable_to_decrypt_some_events_in_poll) + } + @Test fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() { val stringProvider = FakeStringProvider() From fdc28c0383fd61c91770e6fb2623d662ad16e439 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 27 Dec 2022 09:40:58 +0100 Subject: [PATCH 33/75] Add encrypted event id only if not already in the list --- .../aggregation/utd/EncryptedReferenceAggregationProcessor.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt index fdd3bc80e3..8987e837bc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt @@ -43,7 +43,9 @@ class EncryptedReferenceAggregationProcessor @Inject constructor() { ) { event.eventId?.let { eventId -> val existingRelatedPoll = getPollSummaryWithEventId(realm, relatedEventId) - existingRelatedPoll?.encryptedRelatedEventIds?.add(eventId) + if (eventId !in existingRelatedPoll?.encryptedRelatedEventIds.orEmpty()) { + existingRelatedPoll?.encryptedRelatedEventIds?.add(eventId) + } } } From eb4de37603e345b6a9d403163a216cfde7455c62 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 27 Dec 2022 09:55:32 +0100 Subject: [PATCH 34/75] Updating unit tests for poll aggregation processor --- .../poll/DefaultPollAggregationProcessor.kt | 1 - .../DefaultPollAggregationProcessorTest.kt | 54 ++++++++++++++++--- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index 5b25238677..e6815eb0ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -45,7 +45,6 @@ import org.matrix.android.sdk.internal.session.room.relation.poll.FetchPollRespo import org.matrix.android.sdk.internal.task.TaskExecutor import javax.inject.Inject -// TODO update unit tests internal class DefaultPollAggregationProcessor @Inject constructor( private val taskExecutor: TaskExecutor, private val fetchPollResponseEventsTask: FetchPollResponseEventsTask, diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt index 0888d82907..766e51a8e5 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessorTest.kt @@ -25,6 +25,8 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeFalse import org.amshove.kluent.shouldBeTrue +import org.amshove.kluent.shouldContain +import org.amshove.kluent.shouldNotContain import org.junit.Before import org.junit.Test import org.matrix.android.sdk.api.session.Session @@ -105,6 +107,24 @@ class DefaultPollAggregationProcessorTest { pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT).shouldBeTrue() } + @Test + fun `given a poll response event with a reference, when processing, then event id is removed from encrypted events list`() { + // Given + val anotherEventId = "other-event-id" + val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity( + encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId) + ) + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity + + // When + val result = pollAggregationProcessor.handlePollResponseEvent(session, realm.instance, A_POLL_RESPONSE_EVENT) + + // Then + result.shouldBeTrue() + pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID) + pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId) + } + @Test fun `given a poll response event after poll is closed, when processing, then is ignored and returns false`() { every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity().apply { @@ -132,12 +152,33 @@ class DefaultPollAggregationProcessorTest { // Given every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() every { fakeTaskExecutor.instance.executorScope } returns this - - // When val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) + // When + val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT) + // Then - pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() + result.shouldBeTrue() + } + + @Test + fun `given a poll end event, when processing, then event id is removed from encrypted events list`() = runTest { + // Given + val anotherEventId = "other-event-id" + val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity( + encryptedRelatedEventIds = RealmList(AN_EVENT_ID, anotherEventId) + ) + every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns pollResponseAggregatedSummaryEntity + every { fakeTaskExecutor.instance.executorScope } returns this + val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, true) + + // When + val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT) + + // Then + result.shouldBeTrue() + pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldNotContain(AN_EVENT_ID) + pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anotherEventId) } @Test @@ -145,12 +186,13 @@ class DefaultPollAggregationProcessorTest { // Given every { realm.instance.createObject(PollResponseAggregatedSummaryEntity::class.java) } returns PollResponseAggregatedSummaryEntity() every { fakeTaskExecutor.instance.executorScope } returns this - - // When val powerLevelsHelper = mockRedactionPowerLevels(A_USER_ID_1, false) + // When + val result = pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT) + // Then - pollAggregationProcessor.handlePollEndEvent(session, powerLevelsHelper, realm.instance, A_POLL_END_EVENT).shouldBeTrue() + result.shouldBeTrue() } @Test From 1bd11775e92f454bf6d8e8cc2ac45fb4b9954202 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 27 Dec 2022 10:33:25 +0100 Subject: [PATCH 35/75] Adding unit tests for EncryptedReferenceAggregationProcessor --- .../poll/PollAggregationProcessor.kt | 2 +- .../EncryptedReferenceAggregationProcessor.kt | 16 +- ...ryptedReferenceAggregationProcessorTest.kt | 138 ++++++++++++++++++ .../android/sdk/test/fakes/FakeRealm.kt | 8 + 4 files changed, 156 insertions(+), 8 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt index 848643b435..33a69b720a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/PollAggregationProcessor.kt @@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper -interface PollAggregationProcessor { +internal interface PollAggregationProcessor { /** * Poll start events don't need to be processed by the aggregator. * This function will only handle if the poll is edited and will update the poll summary entity. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt index 8987e837bc..43631fcc3e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessor.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,18 +22,20 @@ import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSumm import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields import javax.inject.Inject -class EncryptedReferenceAggregationProcessor @Inject constructor() { +internal class EncryptedReferenceAggregationProcessor @Inject constructor() { - // TODO add unit tests fun handle( realm: Realm, event: Event, isLocalEcho: Boolean, relatedEventId: String? - ) { - if (isLocalEcho || relatedEventId.isNullOrEmpty()) return - - handlePollReference(realm = realm, event = event, relatedEventId = relatedEventId) + ): Boolean { + return if (isLocalEcho || relatedEventId.isNullOrEmpty()) { + false + } else { + handlePollReference(realm = realm, event = event, relatedEventId = relatedEventId) + true + } } private fun handlePollReference( diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt new file mode 100644 index 0000000000..2998b9bff0 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/utd/EncryptedReferenceAggregationProcessorTest.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room.aggregation.utd + +import io.mockk.every +import io.mockk.mockk +import io.realm.RealmList +import org.amshove.kluent.shouldBeFalse +import org.amshove.kluent.shouldBeTrue +import org.amshove.kluent.shouldContain +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.givenContainsValue +import org.matrix.android.sdk.test.fakes.givenFindFirst + +internal class EncryptedReferenceAggregationProcessorTest { + + private val fakeRealm = FakeRealm() + + private val encryptedReferenceAggregationProcessor = EncryptedReferenceAggregationProcessor() + + @Test + fun `given local echo when process then result is false`() { + // Given + val anEvent = mockk() + val isLocalEcho = true + val relatedEventId = "event-id" + + // When + val result = encryptedReferenceAggregationProcessor.handle( + realm = fakeRealm.instance, + event = anEvent, + isLocalEcho = isLocalEcho, + relatedEventId = relatedEventId, + ) + + // Then + result.shouldBeFalse() + } + + @Test + fun `given invalid event id when process then result is false`() { + // Given + val anEvent = mockk() + val isLocalEcho = false + + // When + val result1 = encryptedReferenceAggregationProcessor.handle( + realm = fakeRealm.instance, + event = anEvent, + isLocalEcho = isLocalEcho, + relatedEventId = null, + ) + val result2 = encryptedReferenceAggregationProcessor.handle( + realm = fakeRealm.instance, + event = anEvent, + isLocalEcho = isLocalEcho, + relatedEventId = "", + ) + + // Then + result1.shouldBeFalse() + result2.shouldBeFalse() + } + + @Test + fun `given related event id of an existing poll when process then result is true and event id is stored in poll summary`() { + // Given + val anEventId = "event-id" + val anEvent = givenAnEvent(anEventId) + val isLocalEcho = false + val relatedEventId = "related-event-id" + val pollResponseAggregatedSummaryEntity = PollResponseAggregatedSummaryEntity( + encryptedRelatedEventIds = RealmList(), + ) + fakeRealm.givenWhere() + .givenContainsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, relatedEventId) + .givenFindFirst(pollResponseAggregatedSummaryEntity) + + // When + val result = encryptedReferenceAggregationProcessor.handle( + realm = fakeRealm.instance, + event = anEvent, + isLocalEcho = isLocalEcho, + relatedEventId = relatedEventId, + ) + + // Then + result.shouldBeTrue() + pollResponseAggregatedSummaryEntity.encryptedRelatedEventIds.shouldContain(anEventId) + } + + @Test + fun `given related event id but no existing related poll when process then result is true and event id is not stored`() { + // Given + val anEventId = "event-id" + val anEvent = givenAnEvent(anEventId) + val isLocalEcho = false + val relatedEventId = "related-event-id" + fakeRealm.givenWhere() + .givenContainsValue(PollResponseAggregatedSummaryEntityFields.SOURCE_EVENTS.`$`, relatedEventId) + .givenFindFirst(null) + + // When + val result = encryptedReferenceAggregationProcessor.handle( + realm = fakeRealm.instance, + event = anEvent, + isLocalEcho = isLocalEcho, + relatedEventId = relatedEventId, + ) + + // Then + result.shouldBeTrue() + } + + private fun givenAnEvent(eventId: String): Event { + return mockk().also { + every { it.eventId } returns eventId + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index ba124a86aa..49d64c1835 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -117,6 +117,14 @@ inline fun RealmQuery.givenIn( return this } +inline fun RealmQuery.givenContainsValue( + fieldName: String, + value: String, +): RealmQuery { + every { containsValue(fieldName, value) } returns this + return this +} + /** * Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked. */ From a04c60a85bee8c52e6dc1c94ce5be13283863db0 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 27 Dec 2022 11:57:50 +0100 Subject: [PATCH 36/75] Adding unit tests for EncryptedEventRelationsAggregationProcessor --- .../UnableToDecryptEventLiveProcessor.kt | 8 +- ...yptedEventRelationsAggregationProcessor.kt | 49 ++-- ...dEventRelationsAggregationProcessorTest.kt | 209 ++++++++++++++++++ ...eEncryptedReferenceAggregationProcessor.kt | 42 ++++ 4 files changed, 284 insertions(+), 24 deletions(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessorTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt index c5fa6dc88e..8ff8cec6ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt @@ -21,7 +21,13 @@ import org.matrix.android.sdk.api.session.events.model.Event internal interface UnableToDecryptEventLiveProcessor { - fun process(realm: Realm, event: Event) + /** + * Process the given event. + * @param realm a realm instance + * @param event the event to be processed + * @return true if it has been processed, false if it was ignored. + */ + fun process(realm: Realm, event: Event): Boolean /** * Called after transaction. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt index bae95f1c15..1d0270d8e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt @@ -32,28 +32,27 @@ internal class EncryptedEventRelationsAggregationProcessor @Inject constructor( private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor, ) : UnableToDecryptEventLiveProcessor { - // TODO add unit tests - override fun process(realm: Realm, event: Event) { + override fun process(realm: Realm, event: Event): Boolean { val roomId = event.roomId - if (roomId == null) { + return if (roomId == null) { Timber.w("Event has no room id ${event.eventId}") - return - } + false + } else { + val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") - val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") - - when (event.getClearType()) { - EventType.ENCRYPTED -> { - val encryptedEventContent = event.content.toModel() - processEncryptedContent( - encryptedEventContent = encryptedEventContent, - realm = realm, - event = event, - roomId = roomId, - isLocalEcho = isLocalEcho, - ) + return when (event.getClearType()) { + EventType.ENCRYPTED -> { + val encryptedEventContent = event.content.toModel() + processEncryptedContent( + encryptedEventContent = encryptedEventContent, + realm = realm, + event = event, + roomId = roomId, + isLocalEcho = isLocalEcho, + ) + } + else -> false } - else -> Unit } } @@ -63,30 +62,34 @@ internal class EncryptedEventRelationsAggregationProcessor @Inject constructor( event: Event, roomId: String, isLocalEcho: Boolean, - ) { - when (encryptedEventContent?.relatesTo?.type) { + ): Boolean { + return when (encryptedEventContent?.relatesTo?.type) { RelationType.REPLACE -> { Timber.w("## UTD replace in room $roomId for event ${event.eventId}") + false } RelationType.RESPONSE -> { // can we / should we do we something for UTD response?? Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + false } RelationType.REFERENCE -> { // can we / should we do we something for UTD reference?? Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - encryptedReferenceAggregationProcessor.handle( + val result = encryptedReferenceAggregationProcessor.handle( realm = realm, event = event, isLocalEcho = isLocalEcho, - relatedEventId = encryptedEventContent.relatesTo.eventId + relatedEventId = encryptedEventContent.relatesTo.eventId, ) + result } RelationType.ANNOTATION -> { // can we / should we do we something for UTD annotation?? Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + false } - else -> Unit + else -> false } } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessorTest.kt new file mode 100644 index 0000000000..9d68ed1a77 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessorTest.kt @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room + +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldBeFalse +import org.junit.Test +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.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd.FakeEncryptedReferenceAggregationProcessor + +class EncryptedEventRelationsAggregationProcessorTest { + + private val fakeEncryptedReferenceAggregationProcessor = FakeEncryptedReferenceAggregationProcessor() + private val fakeRealm = FakeRealm() + + private val encryptedEventRelationsAggregationProcessor = EncryptedEventRelationsAggregationProcessor( + encryptedReferenceAggregationProcessor = fakeEncryptedReferenceAggregationProcessor.instance, + ) + + @Test + fun `given no room Id when process then result is false`() { + // Given + val anEvent = givenAnEvent( + eventId = "event-id", + roomId = null, + eventType = EventType.ENCRYPTED, + ) + + // When + val result = encryptedEventRelationsAggregationProcessor.process( + realm = fakeRealm.instance, + event = anEvent, + ) + + // Then + result.shouldBeFalse() + } + + @Test + fun `given an encrypted reference event when process then reference is processed`() { + // Given + val anEvent = givenAnEvent( + eventId = "event-id", + roomId = "room-id", + eventType = EventType.ENCRYPTED, + ) + val relatedEventId = "related-event-id" + val encryptedEventContent = givenEncryptedEventContent( + relationType = RelationType.REFERENCE, + relatedEventId = relatedEventId, + ) + every { anEvent.content } returns encryptedEventContent.toContent() + val resultOfReferenceProcess = false + fakeEncryptedReferenceAggregationProcessor.givenHandleReturns(resultOfReferenceProcess) + + // When + val result = encryptedEventRelationsAggregationProcessor.process( + realm = fakeRealm.instance, + event = anEvent, + ) + + // Then + result shouldBeEqualTo resultOfReferenceProcess + fakeEncryptedReferenceAggregationProcessor.verifyHandle( + realm = fakeRealm.instance, + event = anEvent, + isLocalEcho = false, + relatedEventId = relatedEventId, + ) + } + + @Test + fun `given an encrypted replace event when process then result is false`() { + // Given + val anEvent = givenAnEvent( + eventId = "event-id", + roomId = "room-id", + eventType = EventType.ENCRYPTED, + ) + val relatedEventId = "related-event-id" + val encryptedEventContent = givenEncryptedEventContent( + relationType = RelationType.REPLACE, + relatedEventId = relatedEventId, + ) + every { anEvent.content } returns encryptedEventContent.toContent() + + // When + val result = encryptedEventRelationsAggregationProcessor.process( + realm = fakeRealm.instance, + event = anEvent, + ) + + // Then + result.shouldBeFalse() + } + + @Test + fun `given an encrypted response event when process then result is false`() { + // Given + val anEvent = givenAnEvent( + eventId = "event-id", + roomId = "room-id", + eventType = EventType.ENCRYPTED, + ) + val relatedEventId = "related-event-id" + val encryptedEventContent = givenEncryptedEventContent( + relationType = RelationType.RESPONSE, + relatedEventId = relatedEventId, + ) + every { anEvent.content } returns encryptedEventContent.toContent() + + // When + val result = encryptedEventRelationsAggregationProcessor.process( + realm = fakeRealm.instance, + event = anEvent, + ) + + // Then + result.shouldBeFalse() + } + + @Test + fun `given an encrypted annotation event when process then result is false`() { + // Given + val anEvent = givenAnEvent( + eventId = "event-id", + roomId = "room-id", + eventType = EventType.ENCRYPTED, + ) + val relatedEventId = "related-event-id" + val encryptedEventContent = givenEncryptedEventContent( + relationType = RelationType.ANNOTATION, + relatedEventId = relatedEventId, + ) + every { anEvent.content } returns encryptedEventContent.toContent() + + // When + val result = encryptedEventRelationsAggregationProcessor.process( + realm = fakeRealm.instance, + event = anEvent, + ) + + // Then + result.shouldBeFalse() + } + + @Test + fun `given a non encrypted event when process then result is false`() { + // Given + val anEvent = givenAnEvent( + eventId = "event-id", + roomId = "room-id", + eventType = EventType.MESSAGE, + ) + + // When + val result = encryptedEventRelationsAggregationProcessor.process( + realm = fakeRealm.instance, + event = anEvent, + ) + + // Then + result.shouldBeFalse() + } + + private fun givenAnEvent( + eventId: String, + roomId: String?, + eventType: String, + ): Event { + return mockk().also { + every { it.eventId } returns eventId + every { it.roomId } returns roomId + every { it.getClearType() } returns eventType + } + } + + private fun givenEncryptedEventContent(relationType: String, relatedEventId: String): EncryptedEventContent { + val relationContent = RelationDefaultContent( + eventId = relatedEventId, + type = relationType, + ) + return EncryptedEventContent( + relatesTo = relationContent, + ) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt new file mode 100644 index 0000000000..7661095fe3 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/session/room/aggregation/utd/FakeEncryptedReferenceAggregationProcessor.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.realm.Realm +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor + +internal class FakeEncryptedReferenceAggregationProcessor { + + val instance: EncryptedReferenceAggregationProcessor = mockk() + + fun givenHandleReturns(result: Boolean) { + every { instance.handle(any(), any(), any(), any()) } returns result + } + + fun verifyHandle( + realm: Realm, + event: Event, + isLocalEcho: Boolean, + relatedEventId: String?, + ) { + verify { instance.handle(realm, event, isLocalEcho, relatedEventId) } + } +} From e9f59d85b46a2fc8b84362859b16bab18c0ca756 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 27 Dec 2022 14:38:21 +0100 Subject: [PATCH 37/75] Adding unit tests for DefaultCreateUnableToDecryptEventEntityTask --- .../CreateUnableToDecryptEventEntityTask.kt | 1 - ...reateUnableToDecryptEventEntityTaskTest.kt | 65 +++++++++++++++++++ .../android/sdk/test/fakes/FakeRealm.kt | 8 +++ .../sdk/test/fakes/FakeRealmConfiguration.kt | 6 ++ 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/tasks/DefaultCreateUnableToDecryptEventEntityTaskTest.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt index 17c7ae5ccd..c502e19e35 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt @@ -32,7 +32,6 @@ internal interface CreateUnableToDecryptEventEntityTask : Task { + it is UnableToDecryptEventEntity && it.eventId == anEventId + }) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index 49d64c1835..1f9bc2a976 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -23,6 +23,7 @@ import io.mockk.mockk import io.mockk.runs import io.mockk.verify import io.realm.Realm +import io.realm.Realm.Transaction import io.realm.RealmModel import io.realm.RealmObject import io.realm.RealmQuery @@ -42,6 +43,13 @@ internal class FakeRealm { inline fun verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) { verify { instance.insertOrUpdate(verification()) } } + + fun givenExecuteTransactionAsync() { + every { instance.executeTransactionAsync(any()) } answers { + firstArg().execute(instance) + mockk() + } + } } inline fun RealmQuery.givenFindFirst( diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt index 9ad7032262..3a69515140 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.test.fakes import io.mockk.coEvery +import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.realm.Realm @@ -36,4 +37,9 @@ internal class FakeRealmConfiguration { secondArg<(Realm) -> T>().invoke(realm) } } + + fun givenGetRealmInstance(realm: Realm) { + mockkStatic(Realm::class) + every { Realm.getInstance(instance) } returns realm + } } From b001bc382f5d8dd96aadeec191170c6e14eea3e6 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 27 Dec 2022 15:29:42 +0100 Subject: [PATCH 38/75] Fixing code style issue --- .../room/aggregation/poll/DefaultPollAggregationProcessor.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt index e6815eb0ca..2ff43d6812 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/poll/DefaultPollAggregationProcessor.kt @@ -232,7 +232,7 @@ internal class DefaultPollAggregationProcessor @Inject constructor( } private fun removeEncryptedRelatedEventIdIfNeeded(aggregatedPollSummaryEntity: PollResponseAggregatedSummaryEntity, eventId: String) { - if(aggregatedPollSummaryEntity.encryptedRelatedEventIds.contains(eventId)) { + if (aggregatedPollSummaryEntity.encryptedRelatedEventIds.contains(eventId)) { aggregatedPollSummaryEntity.encryptedRelatedEventIds.remove(eventId) } } From fd58875c31c0c42f4d5c4136c243456ed58b46f1 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Tue, 3 Jan 2023 10:43:34 +0100 Subject: [PATCH 39/75] Database migration --- .../database/RealmSessionStoreMigration.kt | 4 +- .../database/migration/MigrateSessionTo048.kt | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index ba102a7a48..2b7e9a04a1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -64,6 +64,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo044 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo045 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo046 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo047 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo048 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -72,7 +73,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 47L, + schemaVersion = 48L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -129,5 +130,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 45) MigrateSessionTo045(realm).perform() if (oldVersion < 46) MigrateSessionTo046(realm).perform() if (oldVersion < 47) MigrateSessionTo047(realm).perform() + if (oldVersion < 48) MigrateSessionTo048(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt new file mode 100644 index 0000000000..f9bd8ae7aa --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Adding a new field in poll summary to keep track of non decrypted related events. + * Adding a new entity UnableToDecryptEventEntity. + */ +internal class MigrateSessionTo048(realm: DynamicRealm) : RealmMigrator(realm, 47) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("PollResponseAggregatedSummaryEntity") + ?.addRealmListField(PollResponseAggregatedSummaryEntityFields.ENCRYPTED_RELATED_EVENT_IDS.`$`, String::class.java) + + realm.schema.create("UnableToDecryptEventEntity") + ?.addField(UnableToDecryptEventEntityFields.EVENT_ID, String::class.java) + ?.setRequired(UnableToDecryptEventEntityFields.EVENT_ID, true) + } +} From 8b051c5b86cf6dfd12b41dd95b8665af8b2e5eb9 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Fri, 6 Jan 2023 15:19:33 +0100 Subject: [PATCH 40/75] Replace usage of new UnableToDecryptEventEntity by usage of existing EventInsertEntity --- .../sdk/internal/crypto/CryptoModule.kt | 5 - .../sdk/internal/crypto/EventDecryptor.kt | 12 - .../CreateUnableToDecryptEventEntityTask.kt | 45 ---- .../database/EventInsertLiveObserver.kt | 16 +- .../UnableToDecryptEventLiveObserver.kt | 83 ------- .../database/migration/MigrateSessionTo048.kt | 8 +- .../database/model/EventInsertEntity.kt | 2 +- .../database/model/SessionRealmModule.kt | 1 - .../database/query/EventEntityQueries.kt | 8 +- .../sdk/internal/session/SessionModule.kt | 10 - .../UnableToDecryptEventLiveProcessor.kt | 39 ---- ...yptedEventRelationsAggregationProcessor.kt | 95 -------- .../EventRelationsAggregationProcessor.kt | 43 ++++ ...reateUnableToDecryptEventEntityTaskTest.kt | 65 ------ ...dEventRelationsAggregationProcessorTest.kt | 209 ------------------ .../EventRelationsAggregationProcessorTest.kt | 132 +++++++++++ .../android/sdk/test/fakes/FakeRealm.kt | 8 - .../sdk/test/fakes/FakeRealmConfiguration.kt | 6 - .../fakes/internal/FakeEventEditValidator.kt} | 18 +- .../FakeLiveLocationAggregationProcessor.kt | 25 +++ .../internal/FakePollAggregationProcessor.kt | 25 +++ 21 files changed, 252 insertions(+), 603 deletions(-) delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/UnableToDecryptEventLiveObserver.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt delete mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt delete mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/tasks/DefaultCreateUnableToDecryptEventEntityTaskTest.kt delete mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessorTest.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt rename matrix-sdk-android/src/{main/java/org/matrix/android/sdk/internal/database/model/UnableToDecryptEventEntity.kt => test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt} (57%) create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt create mode 100644 matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt index d78f4a3107..c69a859016 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/CryptoModule.kt @@ -62,9 +62,7 @@ import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStore import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreMigration import org.matrix.android.sdk.internal.crypto.store.db.RealmCryptoStoreModule import org.matrix.android.sdk.internal.crypto.tasks.ClaimOneTimeKeysForUsersDeviceTask -import org.matrix.android.sdk.internal.crypto.tasks.CreateUnableToDecryptEventEntityTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultClaimOneTimeKeysForUsersDevice -import org.matrix.android.sdk.internal.crypto.tasks.DefaultCreateUnableToDecryptEventEntityTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultDeleteDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.DefaultDownloadKeysForUsers import org.matrix.android.sdk.internal.crypto.tasks.DefaultEncryptEventTask @@ -255,7 +253,4 @@ internal abstract class CryptoModule { @Binds abstract fun bindSendEventTask(task: DefaultSendEventTask): SendEventTask - - @Binds - abstract fun bindCreateUnableToDecryptEventEntityTask(task: DefaultCreateUnableToDecryptEventEntityTask): CreateUnableToDecryptEventEntityTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt index 524db32670..03672ae81c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/EventDecryptor.kt @@ -38,7 +38,6 @@ import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore -import org.matrix.android.sdk.internal.crypto.tasks.CreateUnableToDecryptEventEntityTask import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask import org.matrix.android.sdk.internal.extensions.foldToCallback import org.matrix.android.sdk.internal.session.SessionScope @@ -61,7 +60,6 @@ internal class EventDecryptor @Inject constructor( private val deviceListManager: DeviceListManager, private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction, private val cryptoStore: IMXCryptoStore, - private val createUnableToDecryptEventEntityTask: CreateUnableToDecryptEventEntityTask, ) { /** @@ -138,7 +136,6 @@ internal class EventDecryptor @Inject constructor( val eventContent = event.content if (eventContent == null) { Timber.tag(loggerTag.value).e("decryptEvent : empty event content") - createUnableToDecryptEventEntity(event.eventId) throw MXCryptoError.Base(MXCryptoError.ErrorType.BAD_ENCRYPTED_MESSAGE, MXCryptoError.BAD_ENCRYPTED_MESSAGE_REASON) } else if (event.isRedacted()) { // we shouldn't attempt to decrypt a redacted event because the content is cleared and decryption will fail because of null algorithm @@ -156,7 +153,6 @@ internal class EventDecryptor @Inject constructor( if (alg == null) { val reason = String.format(MXCryptoError.UNABLE_TO_DECRYPT_REASON, event.eventId, algorithm) Timber.tag(loggerTag.value).e("decryptEvent() : $reason") - createUnableToDecryptEventEntity(event.eventId) throw MXCryptoError.Base(MXCryptoError.ErrorType.UNABLE_TO_DECRYPT, reason) } else { try { @@ -175,20 +171,12 @@ internal class EventDecryptor @Inject constructor( } } } - createUnableToDecryptEventEntity(event.eventId) throw mxCryptoError } } } } - private suspend fun createUnableToDecryptEventEntity(eventId: String?) { - eventId?.let { - val params = CreateUnableToDecryptEventEntityTask.Params(eventId = it) - createUnableToDecryptEventEntityTask.execute(params) - } - } - private suspend fun markOlmSessionForUnwedging(senderId: String, senderKey: String) { wedgedMutex.withLock { val info = WedgedDeviceInfo(senderId, senderKey) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt deleted file mode 100644 index c502e19e35..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/CreateUnableToDecryptEventEntityTask.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto.tasks - -import io.realm.RealmConfiguration -import org.matrix.android.sdk.internal.crypto.store.db.doRealmTransactionAsync -import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntity -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.task.Task -import javax.inject.Inject - -/** - * This task create a dedicated entity for UTD events so that it can be processed later. - */ -internal interface CreateUnableToDecryptEventEntityTask : Task { - data class Params( - val eventId: String, - ) -} - -internal class DefaultCreateUnableToDecryptEventEntityTask @Inject constructor( - @SessionDatabase val realmConfiguration: RealmConfiguration, -) : CreateUnableToDecryptEventEntityTask { - - override suspend fun execute(params: CreateUnableToDecryptEventEntityTask.Params) { - val utdEventEntity = UnableToDecryptEventEntity(eventId = params.eventId) - doRealmTransactionAsync(realmConfiguration) { realm -> - realm.insert(utdEventEntity) - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index d1ca4f48a6..84fb14c914 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -22,6 +22,7 @@ import io.realm.RealmResults import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.matrix.android.sdk.api.session.events.model.EventType 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.model.EventInsertEntity @@ -34,7 +35,7 @@ import javax.inject.Inject internal class EventInsertLiveObserver @Inject constructor( @SessionDatabase realmConfiguration: RealmConfiguration, - private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor> + private val processors: Set<@JvmSuppressWildcards EventInsertLiveProcessor>, ) : RealmLiveEntityObserver(realmConfiguration) { @@ -51,6 +52,7 @@ internal class EventInsertLiveObserver @Inject constructor( return@withLock } val idsToDeleteAfterProcess = ArrayList() + val idsOfEncryptedEvents = ArrayList() val filteredEvents = ArrayList(results.size) Timber.v("EventInsertEntity updated with ${results.size} results in db") results.forEach { @@ -64,7 +66,11 @@ internal class EventInsertLiveObserver @Inject constructor( } filteredEvents.add(copiedEvent) } - idsToDeleteAfterProcess.add(it.eventId) + if (it.eventType == EventType.ENCRYPTED) { + idsOfEncryptedEvents.add(it.eventId) + } else { + idsToDeleteAfterProcess.add(it.eventId) + } } awaitTransaction(realmConfiguration) { realm -> Timber.v("##Transaction: There are ${filteredEvents.size} events to process ") @@ -86,6 +92,12 @@ internal class EventInsertLiveObserver @Inject constructor( .`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray()) .findAll() .deleteAllFromRealm() + + // make the encrypted events not processable: they will be processed again after decryption + realm.where(EventInsertEntity::class.java) + .`in`(EventInsertEntityFields.EVENT_ID, idsOfEncryptedEvents.toTypedArray()) + .findAll() + .forEach { it.canBeProcessed = false } } processors.forEach { it.onPostProcess() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/UnableToDecryptEventLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/UnableToDecryptEventLiveObserver.kt deleted file mode 100644 index 7835a16ad1..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/UnableToDecryptEventLiveObserver.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.database - -import com.zhuinden.monarchy.Monarchy -import io.realm.RealmConfiguration -import io.realm.RealmResults -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -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.model.UnableToDecryptEventEntity -import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntityFields -import org.matrix.android.sdk.internal.database.query.where -import org.matrix.android.sdk.internal.di.SessionDatabase -import org.matrix.android.sdk.internal.session.UnableToDecryptEventLiveProcessor -import timber.log.Timber -import javax.inject.Inject - -internal class UnableToDecryptEventLiveObserver @Inject constructor( - @SessionDatabase realmConfiguration: RealmConfiguration, - private val processors: Set<@JvmSuppressWildcards UnableToDecryptEventLiveProcessor> -) : - RealmLiveEntityObserver(realmConfiguration) { - - private val lock = Mutex() - - override val query = Monarchy.Query { - it.where(UnableToDecryptEventEntity::class.java) - } - - override fun onChange(results: RealmResults) { - observerScope.launch { - lock.withLock { - if (!results.isLoaded || results.isEmpty()) { - return@withLock - } - val copiedEvents = ArrayList(results.size) - Timber.v("UnableToDecryptEventEntity updated with ${results.size} results in db") - results.forEach { - // don't use copy from realm over there - val copiedEvent = UnableToDecryptEventEntity(eventId = it.eventId) - copiedEvents.add(copiedEvent) - } - awaitTransaction(realmConfiguration) { realm -> - Timber.v("##Transaction: There are ${copiedEvents.size} events to process ") - copiedEvents.forEach { utdEvent -> - val eventId = utdEvent.eventId - val event = EventEntity.where(realm, eventId).findFirst() - if (event == null) { - Timber.v("Event $eventId not found") - return@forEach - } - val domainEvent = event.asDomain() - processors.forEach { - it.process(realm, domainEvent) - } - } - realm.where(UnableToDecryptEventEntity::class.java) - .`in`(UnableToDecryptEventEntityFields.EVENT_ID, copiedEvents.map { it.eventId }.toTypedArray()) - .findAll() - .deleteAllFromRealm() - } - processors.forEach { it.onPostProcess() } - } - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt index f9bd8ae7aa..4299054c56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo048.kt @@ -18,21 +18,15 @@ package org.matrix.android.sdk.internal.database.migration import io.realm.DynamicRealm import org.matrix.android.sdk.internal.database.model.PollResponseAggregatedSummaryEntityFields -import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntityFields import org.matrix.android.sdk.internal.util.database.RealmMigrator /** * Adding a new field in poll summary to keep track of non decrypted related events. - * Adding a new entity UnableToDecryptEventEntity. */ -internal class MigrateSessionTo048(realm: DynamicRealm) : RealmMigrator(realm, 47) { +internal class MigrateSessionTo048(realm: DynamicRealm) : RealmMigrator(realm, 48) { override fun doMigrate(realm: DynamicRealm) { realm.schema.get("PollResponseAggregatedSummaryEntity") ?.addRealmListField(PollResponseAggregatedSummaryEntityFields.ENCRYPTED_RELATED_EVENT_IDS.`$`, String::class.java) - - realm.schema.create("UnableToDecryptEventEntity") - ?.addField(UnableToDecryptEventEntityFields.EVENT_ID, String::class.java) - ?.setRequired(UnableToDecryptEventEntityFields.EVENT_ID, true) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt index eff332dc3a..054094c398 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/EventInsertEntity.kt @@ -27,7 +27,7 @@ internal open class EventInsertEntity( var eventType: String = "", /** * This flag will be used to filter EventInsertEntity in EventInsertLiveObserver. - * Currently it's set to false when the event content is encrypted. + * Currently it's set to false after an event with encrypted content has been processed. */ var canBeProcessed: Boolean = true ) : RealmObject() { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt index 79b0dd699c..93fe1bd1d2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SessionRealmModule.kt @@ -73,7 +73,6 @@ import org.matrix.android.sdk.internal.database.model.threads.ThreadSummaryEntit UserPresenceEntity::class, ThreadSummaryEntity::class, ThreadListPageEntity::class, - UnableToDecryptEventEntity::class, ] ) internal class SessionRealmModule diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt index 0f1c226044..4805c36f8c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventEntityQueries.kt @@ -20,7 +20,6 @@ import io.realm.Realm import io.realm.RealmList import io.realm.RealmQuery import io.realm.kotlin.where -import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.internal.database.model.EventEntity import org.matrix.android.sdk.internal.database.model.EventEntityFields import org.matrix.android.sdk.internal.database.model.EventInsertEntity @@ -32,10 +31,9 @@ internal fun EventEntity.copyToRealmOrIgnore(realm: Realm, insertType: EventInse .equalTo(EventEntityFields.ROOM_ID, roomId) .findFirst() return if (eventEntity == null) { - val canBeProcessed = type != EventType.ENCRYPTED || decryptionResultJson != null - val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = canBeProcessed).apply { - this.insertType = insertType - } + val insertEntity = EventInsertEntity(eventId = eventId, eventType = type, canBeProcessed = true) + insertEntity.insertType = insertType + realm.insert(insertEntity) // copy this event entity and return it realm.copyToRealm(this) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt index d33112a922..b9f56cbc9f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionModule.kt @@ -50,7 +50,6 @@ import org.matrix.android.sdk.internal.crypto.tasks.RedactEventTask import org.matrix.android.sdk.internal.database.EventInsertLiveObserver import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.SessionRealmConfigurationFactory -import org.matrix.android.sdk.internal.database.UnableToDecryptEventLiveObserver import org.matrix.android.sdk.internal.di.Authenticated import org.matrix.android.sdk.internal.di.CacheDirectory import org.matrix.android.sdk.internal.di.DeviceId @@ -85,7 +84,6 @@ import org.matrix.android.sdk.internal.session.identity.DefaultIdentityService import org.matrix.android.sdk.internal.session.integrationmanager.IntegrationManager import org.matrix.android.sdk.internal.session.openid.DefaultOpenIdService import org.matrix.android.sdk.internal.session.permalinks.DefaultPermalinkService -import org.matrix.android.sdk.internal.session.room.EncryptedEventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor @@ -348,10 +346,6 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventInsertObserver(observer: EventInsertLiveObserver): SessionLifecycleObserver - @Binds - @IntoSet - abstract fun bindUnableToDecryptEventObserver(observer: UnableToDecryptEventLiveObserver): SessionLifecycleObserver - @Binds @IntoSet abstract fun bindIntegrationManager(manager: IntegrationManager): SessionLifecycleObserver @@ -411,8 +405,4 @@ internal abstract class SessionModule { @Binds abstract fun bindPollAggregationProcessor(processor: DefaultPollAggregationProcessor): PollAggregationProcessor - - @Binds - @IntoSet - abstract fun bindEncryptedEventRelationsAggregationProcessor(processor: EncryptedEventRelationsAggregationProcessor): UnableToDecryptEventLiveProcessor } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt deleted file mode 100644 index 8ff8cec6ce..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/UnableToDecryptEventLiveProcessor.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2022 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.session - -import io.realm.Realm -import org.matrix.android.sdk.api.session.events.model.Event - -internal interface UnableToDecryptEventLiveProcessor { - - /** - * Process the given event. - * @param realm a realm instance - * @param event the event to be processed - * @return true if it has been processed, false if it was ignored. - */ - fun process(realm: Realm, event: Event): Boolean - - /** - * Called after transaction. - * Maybe you prefer to process the events outside of the realm transaction. - */ - suspend fun onPostProcess() { - // Noop by default - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt deleted file mode 100644 index 1d0270d8e4..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessor.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.session.room - -import io.realm.Realm -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.LocalEcho -import org.matrix.android.sdk.api.session.events.model.RelationType -import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.internal.session.UnableToDecryptEventLiveProcessor -import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor -import timber.log.Timber -import javax.inject.Inject - -internal class EncryptedEventRelationsAggregationProcessor @Inject constructor( - private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor, -) : UnableToDecryptEventLiveProcessor { - - override fun process(realm: Realm, event: Event): Boolean { - val roomId = event.roomId - return if (roomId == null) { - Timber.w("Event has no room id ${event.eventId}") - false - } else { - val isLocalEcho = LocalEcho.isLocalEchoId(event.eventId ?: "") - - return when (event.getClearType()) { - EventType.ENCRYPTED -> { - val encryptedEventContent = event.content.toModel() - processEncryptedContent( - encryptedEventContent = encryptedEventContent, - realm = realm, - event = event, - roomId = roomId, - isLocalEcho = isLocalEcho, - ) - } - else -> false - } - } - } - - private fun processEncryptedContent( - encryptedEventContent: EncryptedEventContent?, - realm: Realm, - event: Event, - roomId: String, - isLocalEcho: Boolean, - ): Boolean { - return when (encryptedEventContent?.relatesTo?.type) { - RelationType.REPLACE -> { - Timber.w("## UTD replace in room $roomId for event ${event.eventId}") - false - } - RelationType.RESPONSE -> { - // can we / should we do we something for UTD response?? - Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - false - } - RelationType.REFERENCE -> { - // can we / should we do we something for UTD reference?? - Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - val result = encryptedReferenceAggregationProcessor.handle( - realm = realm, - event = event, - isLocalEcho = isLocalEcho, - relatedEventId = encryptedEventContent.relatesTo.eventId, - ) - result - } - RelationType.ANNOTATION -> { - // can we / should we do we something for UTD annotation?? - Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") - false - } - else -> false - } - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt index 0734b286b3..edc10bd187 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessor.kt @@ -23,6 +23,7 @@ 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.LocalEcho import org.matrix.android.sdk.api.session.events.model.RelationType +import org.matrix.android.sdk.api.session.events.model.content.EncryptedEventContent import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel @@ -60,6 +61,7 @@ import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor +import org.matrix.android.sdk.internal.session.room.aggregation.utd.EncryptedReferenceAggregationProcessor import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber @@ -72,6 +74,7 @@ internal class EventRelationsAggregationProcessor @Inject constructor( private val sessionManager: SessionManager, private val liveLocationAggregationProcessor: LiveLocationAggregationProcessor, private val pollAggregationProcessor: PollAggregationProcessor, + private val encryptedReferenceAggregationProcessor: EncryptedReferenceAggregationProcessor, private val editValidator: EventEditValidator, private val clock: Clock, ) : EventInsertLiveProcessor { @@ -139,6 +142,16 @@ internal class EventRelationsAggregationProcessor @Inject constructor( Timber.v("###REACTION in room $roomId , reaction eventID ${event.eventId}") handleReaction(realm, event, roomId, isLocalEcho) } + EventType.ENCRYPTED -> { + val encryptedEventContent = event.content.toModel() + processEncryptedContent( + encryptedEventContent = encryptedEventContent, + realm = realm, + event = event, + roomId = roomId, + isLocalEcho = isLocalEcho, + ) + } EventType.MESSAGE -> { if (event.unsignedData?.relations?.annotations != null) { Timber.v("###REACTION Aggregation in room $roomId for event ${event.eventId}") @@ -223,6 +236,36 @@ internal class EventRelationsAggregationProcessor @Inject constructor( } } + private fun processEncryptedContent( + encryptedEventContent: EncryptedEventContent?, + realm: Realm, + event: Event, + roomId: String, + isLocalEcho: Boolean, + ) { + when (encryptedEventContent?.relatesTo?.type) { + RelationType.REPLACE -> { + Timber.w("## UTD replace in room $roomId for event ${event.eventId}") + } + RelationType.RESPONSE -> { + Timber.w("## UTD response in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + } + RelationType.REFERENCE -> { + Timber.w("## UTD reference in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + encryptedReferenceAggregationProcessor.handle( + realm = realm, + event = event, + isLocalEcho = isLocalEcho, + relatedEventId = encryptedEventContent.relatesTo.eventId, + ) + } + RelationType.ANNOTATION -> { + Timber.w("## UTD annotation in room $roomId related to ${encryptedEventContent.relatesTo.eventId}") + } + else -> Unit + } + } + // OPT OUT serer aggregation until API mature enough private val SHOULD_HANDLE_SERVER_AGREGGATION = false // should be true to work with e2e diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/tasks/DefaultCreateUnableToDecryptEventEntityTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/tasks/DefaultCreateUnableToDecryptEventEntityTaskTest.kt deleted file mode 100644 index 85032d809c..0000000000 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/crypto/tasks/DefaultCreateUnableToDecryptEventEntityTaskTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.crypto.tasks - -import io.mockk.unmockkAll -import io.mockk.verify -import io.realm.RealmModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Test -import org.matrix.android.sdk.internal.database.model.UnableToDecryptEventEntity -import org.matrix.android.sdk.test.fakes.FakeRealm -import org.matrix.android.sdk.test.fakes.FakeRealmConfiguration - -@OptIn(ExperimentalCoroutinesApi::class) -internal class DefaultCreateUnableToDecryptEventEntityTaskTest { - - private val fakeRealmConfiguration = FakeRealmConfiguration() - - private val defaultCreateUnableToDecryptEventEntityTask = DefaultCreateUnableToDecryptEventEntityTask( - realmConfiguration = fakeRealmConfiguration.instance, - ) - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `given an event id when execute then insert entity into database`() = runTest { - // Given - val anEventId = "event-id" - val params = CreateUnableToDecryptEventEntityTask.Params( - eventId = anEventId, - ) - val fakeRealm = FakeRealm() - fakeRealm.givenExecuteTransactionAsync() - fakeRealmConfiguration.givenGetRealmInstance(fakeRealm.instance) - - // When - defaultCreateUnableToDecryptEventEntityTask.execute(params) - - // Then - verify { - fakeRealm.instance.insert(match { - it is UnableToDecryptEventEntity && it.eventId == anEventId - }) - } - } -} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessorTest.kt deleted file mode 100644 index 9d68ed1a77..0000000000 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EncryptedEventRelationsAggregationProcessorTest.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.session.room - -import io.mockk.every -import io.mockk.mockk -import org.amshove.kluent.shouldBeEqualTo -import org.amshove.kluent.shouldBeFalse -import org.junit.Test -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.content.EncryptedEventContent -import org.matrix.android.sdk.api.session.events.model.toContent -import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent -import org.matrix.android.sdk.test.fakes.FakeRealm -import org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd.FakeEncryptedReferenceAggregationProcessor - -class EncryptedEventRelationsAggregationProcessorTest { - - private val fakeEncryptedReferenceAggregationProcessor = FakeEncryptedReferenceAggregationProcessor() - private val fakeRealm = FakeRealm() - - private val encryptedEventRelationsAggregationProcessor = EncryptedEventRelationsAggregationProcessor( - encryptedReferenceAggregationProcessor = fakeEncryptedReferenceAggregationProcessor.instance, - ) - - @Test - fun `given no room Id when process then result is false`() { - // Given - val anEvent = givenAnEvent( - eventId = "event-id", - roomId = null, - eventType = EventType.ENCRYPTED, - ) - - // When - val result = encryptedEventRelationsAggregationProcessor.process( - realm = fakeRealm.instance, - event = anEvent, - ) - - // Then - result.shouldBeFalse() - } - - @Test - fun `given an encrypted reference event when process then reference is processed`() { - // Given - val anEvent = givenAnEvent( - eventId = "event-id", - roomId = "room-id", - eventType = EventType.ENCRYPTED, - ) - val relatedEventId = "related-event-id" - val encryptedEventContent = givenEncryptedEventContent( - relationType = RelationType.REFERENCE, - relatedEventId = relatedEventId, - ) - every { anEvent.content } returns encryptedEventContent.toContent() - val resultOfReferenceProcess = false - fakeEncryptedReferenceAggregationProcessor.givenHandleReturns(resultOfReferenceProcess) - - // When - val result = encryptedEventRelationsAggregationProcessor.process( - realm = fakeRealm.instance, - event = anEvent, - ) - - // Then - result shouldBeEqualTo resultOfReferenceProcess - fakeEncryptedReferenceAggregationProcessor.verifyHandle( - realm = fakeRealm.instance, - event = anEvent, - isLocalEcho = false, - relatedEventId = relatedEventId, - ) - } - - @Test - fun `given an encrypted replace event when process then result is false`() { - // Given - val anEvent = givenAnEvent( - eventId = "event-id", - roomId = "room-id", - eventType = EventType.ENCRYPTED, - ) - val relatedEventId = "related-event-id" - val encryptedEventContent = givenEncryptedEventContent( - relationType = RelationType.REPLACE, - relatedEventId = relatedEventId, - ) - every { anEvent.content } returns encryptedEventContent.toContent() - - // When - val result = encryptedEventRelationsAggregationProcessor.process( - realm = fakeRealm.instance, - event = anEvent, - ) - - // Then - result.shouldBeFalse() - } - - @Test - fun `given an encrypted response event when process then result is false`() { - // Given - val anEvent = givenAnEvent( - eventId = "event-id", - roomId = "room-id", - eventType = EventType.ENCRYPTED, - ) - val relatedEventId = "related-event-id" - val encryptedEventContent = givenEncryptedEventContent( - relationType = RelationType.RESPONSE, - relatedEventId = relatedEventId, - ) - every { anEvent.content } returns encryptedEventContent.toContent() - - // When - val result = encryptedEventRelationsAggregationProcessor.process( - realm = fakeRealm.instance, - event = anEvent, - ) - - // Then - result.shouldBeFalse() - } - - @Test - fun `given an encrypted annotation event when process then result is false`() { - // Given - val anEvent = givenAnEvent( - eventId = "event-id", - roomId = "room-id", - eventType = EventType.ENCRYPTED, - ) - val relatedEventId = "related-event-id" - val encryptedEventContent = givenEncryptedEventContent( - relationType = RelationType.ANNOTATION, - relatedEventId = relatedEventId, - ) - every { anEvent.content } returns encryptedEventContent.toContent() - - // When - val result = encryptedEventRelationsAggregationProcessor.process( - realm = fakeRealm.instance, - event = anEvent, - ) - - // Then - result.shouldBeFalse() - } - - @Test - fun `given a non encrypted event when process then result is false`() { - // Given - val anEvent = givenAnEvent( - eventId = "event-id", - roomId = "room-id", - eventType = EventType.MESSAGE, - ) - - // When - val result = encryptedEventRelationsAggregationProcessor.process( - realm = fakeRealm.instance, - event = anEvent, - ) - - // Then - result.shouldBeFalse() - } - - private fun givenAnEvent( - eventId: String, - roomId: String?, - eventType: String, - ): Event { - return mockk().also { - every { it.eventId } returns eventId - every { it.roomId } returns roomId - every { it.getClearType() } returns eventType - } - } - - private fun givenEncryptedEventContent(relationType: String, relatedEventId: String): EncryptedEventContent { - val relationContent = RelationDefaultContent( - eventId = relatedEventId, - type = relationType, - ) - return EncryptedEventContent( - relatesTo = relationContent, - ) - } -} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt new file mode 100644 index 0000000000..ff803c4f1a --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/EventRelationsAggregationProcessorTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.session.room + +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +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.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.relation.RelationDefaultContent +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeClock +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst +import org.matrix.android.sdk.test.fakes.internal.FakeEventEditValidator +import org.matrix.android.sdk.test.fakes.internal.FakeLiveLocationAggregationProcessor +import org.matrix.android.sdk.test.fakes.internal.FakePollAggregationProcessor +import org.matrix.android.sdk.test.fakes.internal.FakeSessionManager +import org.matrix.android.sdk.test.fakes.internal.session.room.aggregation.utd.FakeEncryptedReferenceAggregationProcessor + +private const val A_ROOM_ID = "room-id" +private const val AN_EVENT_ID = "event-id" + +internal class EventRelationsAggregationProcessorTest { + + private val fakeStateEventDataSource = FakeStateEventDataSource() + private val fakeSessionManager = FakeSessionManager() + private val fakeLiveLocationAggregationProcessor = FakeLiveLocationAggregationProcessor() + private val fakePollAggregationProcessor = FakePollAggregationProcessor() + private val fakeEncryptedReferenceAggregationProcessor = FakeEncryptedReferenceAggregationProcessor() + private val fakeEventEditValidator = FakeEventEditValidator() + private val fakeClock = FakeClock() + private val fakeRealm = FakeRealm() + + private val encryptedEventRelationsAggregationProcessor = EventRelationsAggregationProcessor( + userId = "userId", + stateEventDataSource = fakeStateEventDataSource.instance, + sessionId = "sessionId", + sessionManager = fakeSessionManager.instance, + liveLocationAggregationProcessor = fakeLiveLocationAggregationProcessor.instance, + pollAggregationProcessor = fakePollAggregationProcessor.instance, + encryptedReferenceAggregationProcessor = fakeEncryptedReferenceAggregationProcessor.instance, + editValidator = fakeEventEditValidator.instance, + clock = fakeClock, + ) + + @Test + fun `given an encrypted reference event when process then reference is processed`() { + // Given + val anEvent = givenAnEvent( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + eventType = EventType.ENCRYPTED, + ) + val relatedEventId = "related-event-id" + val encryptedEventContent = givenEncryptedEventContent( + relationType = RelationType.REFERENCE, + relatedEventId = relatedEventId, + ) + every { anEvent.content } returns encryptedEventContent.toContent() + val resultOfReferenceProcess = false + fakeEncryptedReferenceAggregationProcessor.givenHandleReturns(resultOfReferenceProcess) + givenEventAnnotationsSummary(roomId = A_ROOM_ID, eventId = AN_EVENT_ID, annotationsSummary = null) + + // When + encryptedEventRelationsAggregationProcessor.process( + realm = fakeRealm.instance, + event = anEvent, + ) + + // Then + fakeEncryptedReferenceAggregationProcessor.verifyHandle( + realm = fakeRealm.instance, + event = anEvent, + isLocalEcho = false, + relatedEventId = relatedEventId, + ) + } + + private fun givenAnEvent( + eventId: String, + roomId: String?, + eventType: String, + ): Event { + return mockk().also { + every { it.eventId } returns eventId + every { it.roomId } returns roomId + every { it.getClearType() } returns eventType + } + } + + private fun givenEncryptedEventContent(relationType: String, relatedEventId: String): EncryptedEventContent { + val relationContent = RelationDefaultContent( + eventId = relatedEventId, + type = relationType, + ) + return EncryptedEventContent( + relatesTo = relationContent, + ) + } + + private fun givenEventAnnotationsSummary( + roomId: String, + eventId: String, + annotationsSummary: EventAnnotationsSummaryEntity? + ) { + fakeRealm.givenWhere() + .givenEqualTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) + .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId) + .givenFindFirst(annotationsSummary) + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index 1f9bc2a976..49d64c1835 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -23,7 +23,6 @@ import io.mockk.mockk import io.mockk.runs import io.mockk.verify import io.realm.Realm -import io.realm.Realm.Transaction import io.realm.RealmModel import io.realm.RealmObject import io.realm.RealmQuery @@ -43,13 +42,6 @@ internal class FakeRealm { inline fun verifyInsertOrUpdate(crossinline verification: MockKVerificationScope.() -> T) { verify { instance.insertOrUpdate(verification()) } } - - fun givenExecuteTransactionAsync() { - every { instance.executeTransactionAsync(any()) } answers { - firstArg().execute(instance) - mockk() - } - } } inline fun RealmQuery.givenFindFirst( diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt index 3a69515140..9ad7032262 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.test.fakes import io.mockk.coEvery -import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.realm.Realm @@ -37,9 +36,4 @@ internal class FakeRealmConfiguration { secondArg<(Realm) -> T>().invoke(realm) } } - - fun givenGetRealmInstance(realm: Realm) { - mockkStatic(Realm::class) - every { Realm.getInstance(instance) } returns realm - } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UnableToDecryptEventEntity.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt similarity index 57% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UnableToDecryptEventEntity.kt rename to matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt index 6393877d38..2fa36cf60d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/UnableToDecryptEventEntity.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeEventEditValidator.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +14,12 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.database.model +package org.matrix.android.sdk.test.fakes.internal -import io.realm.RealmObject +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.EventEditValidator -/** - * This class is used to get notification on new UTD events. Since these events cannot be processed - * in EventInsertEntity, we should introduce a dedicated entity for that. - */ -internal open class UnableToDecryptEventEntity( - var eventId: String = "", -) : RealmObject() +internal class FakeEventEditValidator { + + val instance: EventEditValidator = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt new file mode 100644 index 0000000000..6385110963 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakeLiveLocationAggregationProcessor.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.aggregation.livelocation.LiveLocationAggregationProcessor + +internal class FakeLiveLocationAggregationProcessor { + + val instance: LiveLocationAggregationProcessor = mockk() +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt new file mode 100644 index 0000000000..5187c785ca --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/internal/FakePollAggregationProcessor.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.test.fakes.internal + +import io.mockk.mockk +import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor + +internal class FakePollAggregationProcessor { + + val instance: PollAggregationProcessor = mockk() +} From 94dd599f137906d611d51d6cc02646de454eb833 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 12 Jan 2023 10:03:01 +0100 Subject: [PATCH 41/75] Fix after rebase --- .../room/detail/timeline/helper/PollResponseDataFactory.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt index 533397b4d8..0ef60da6de 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt @@ -44,7 +44,8 @@ class PollResponseDataFactory @Inject constructor( ) }, winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0, - totalVotes = it.aggregatedContent?.totalVotes ?: 0 + totalVotes = it.aggregatedContent?.totalVotes ?: 0, + hasDecryptionError = it.encryptedRelatedEventIds.isNotEmpty(), ) } } From d1ce15bf18ea4119ece633dc4b42cf08cc5a3316 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 12 Jan 2023 10:14:47 +0100 Subject: [PATCH 42/75] Renaming field in PollResponseData for better clarity --- .../room/detail/timeline/factory/PollItemViewStateFactory.kt | 4 ++-- .../room/detail/timeline/helper/PollResponseDataFactory.kt | 2 +- .../home/room/detail/timeline/item/MessageInformationData.kt | 2 +- .../detail/timeline/factory/PollItemViewStateFactoryTest.kt | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index 28c560e161..7abc51fa51 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -83,7 +83,7 @@ class PollItemViewStateFactory @Inject constructor( totalVotes: Int, winnerVoteCount: Int?, ): PollViewState { - val totalVotesText = if (pollResponseSummary?.hasDecryptionError.orFalse()) { + val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) { stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll) } else { stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes) @@ -131,7 +131,7 @@ class PollItemViewStateFactory @Inject constructor( pollResponseSummary: PollResponseData?, totalVotes: Int ): PollViewState { - val totalVotesText = if (pollResponseSummary?.hasDecryptionError.orFalse()) { + val totalVotesText = if (pollResponseSummary?.hasEncryptedRelatedEvents.orFalse()) { stringProvider.getString(R.string.unable_to_decrypt_some_events_in_poll) } else { stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt index 0ef60da6de..8f81adcd32 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/PollResponseDataFactory.kt @@ -45,7 +45,7 @@ class PollResponseDataFactory @Inject constructor( }, winnerVoteCount = it.aggregatedContent?.winnerVoteCount ?: 0, totalVotes = it.aggregatedContent?.totalVotes ?: 0, - hasDecryptionError = it.encryptedRelatedEventIds.isNotEmpty(), + hasEncryptedRelatedEvents = it.encryptedRelatedEventIds.isNotEmpty(), ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 4bdfb948ce..a1a214785e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -91,7 +91,7 @@ data class PollResponseData( val totalVotes: Int = 0, val winnerVoteCount: Int = 0, val isClosed: Boolean = false, - val hasDecryptionError: Boolean = false, + val hasEncryptedRelatedEvents: Boolean = false, ) : Parcelable { fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt index 4d14458e7f..8ee55d5b6e 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactoryTest.kt @@ -136,7 +136,7 @@ class PollItemViewStateFactoryTest { // Given val stringProvider = FakeStringProvider() val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance) - val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasDecryptionError = true) + val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true, hasEncryptedRelatedEvents = true) val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary) // When @@ -220,7 +220,7 @@ class PollItemViewStateFactoryTest { totalVotes = 1, myVote = A_POLL_OPTION_IDS[0], votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0)), - hasDecryptionError = true, + hasEncryptedRelatedEvents = true, ) val disclosedPollContent = A_POLL_CONTENT.copy( unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( From ee3cbd988481d0d071726831c720389fd60c176a Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Thu, 12 Jan 2023 11:15:38 +0100 Subject: [PATCH 43/75] Filter in only encrypted events with relatesTo content --- .../database/EventInsertLiveObserver.kt | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index 84fb14c914..2cae6c09ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -17,12 +17,16 @@ package org.matrix.android.sdk.internal.database import com.zhuinden.monarchy.Monarchy +import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmResults import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +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.content.EncryptedEventContent +import org.matrix.android.sdk.api.session.events.model.toModel 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.model.EventInsertEntity @@ -76,17 +80,17 @@ internal class EventInsertLiveObserver @Inject constructor( Timber.v("##Transaction: There are ${filteredEvents.size} events to process ") filteredEvents.forEach { eventInsert -> val eventId = eventInsert.eventId - val event = EventEntity.where(realm, eventId).findFirst() - if (event == null) { - Timber.v("Event $eventId not found") + val event = getEvent(realm, eventId) + if (event != null && canProcessEvent(event)) { + processors.filter { + it.shouldProcess(eventId, event.getClearType(), eventInsert.insertType) + }.forEach { + it.process(realm, event) + } + } else { + Timber.v("Cannot process event with id $eventId") return@forEach } - val domainEvent = event.asDomain() - processors.filter { - it.shouldProcess(eventId, domainEvent.getClearType(), eventInsert.insertType) - }.forEach { - it.process(realm, domainEvent) - } } realm.where(EventInsertEntity::class.java) .`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray()) @@ -104,6 +108,20 @@ internal class EventInsertLiveObserver @Inject constructor( } } + private fun getEvent(realm: Realm, eventId: String): Event? { + val event = EventEntity.where(realm, eventId).findFirst() + if (event == null) { + Timber.v("Event $eventId not found") + } + return event?.asDomain() + } + + private fun canProcessEvent(event: Event): Boolean { + // event should be either not encrypted or if encrypted it should contain relatesTo content + return event.getClearType() != EventType.ENCRYPTED || + event.content.toModel()?.relatesTo != null + } + private fun shouldProcess(eventInsertEntity: EventInsertEntity): Boolean { return processors.any { it.shouldProcess(eventInsertEntity.eventId, eventInsertEntity.eventType, eventInsertEntity.insertType) From 90d9eaf9507ee5f456c452dc7d88c1485ca84d3e Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Mon, 9 Jan 2023 07:09:04 +0000 Subject: [PATCH 44/75] Translated using Weblate (Czech) Currently translated at 100.0% (2585 of 2585 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/cs/ --- library/ui-strings/src/main/res/values-cs/strings.xml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values-cs/strings.xml b/library/ui-strings/src/main/res/values-cs/strings.xml index 0a7998deaa..2d2b91d645 100644 --- a/library/ui-strings/src/main/res/values-cs/strings.xml +++ b/library/ui-strings/src/main/res/values-cs/strings.xml @@ -2860,7 +2860,7 @@ Přihlásit se pomocí QR kódu Naskenovat QR kód Možnost nahrávat a odesílat hlasové vysílání na časové ose místnosti. - Povolit hlasové vysílání (v aktivním vývoji) + Povolit hlasové vysílání Domovský server nepodporuje přihlášení pomocí QR kódu. Přihlášení bylo na druhém zařízení zrušeno. Tento QR kód je neplatný. @@ -2946,4 +2946,13 @@ Odkaz Text Nastavit odkaz + Přístupový token umožňuje plný přístup k účtu. Nikomu ho nesdělujte. + Přístupový token + Přepnout na odrážky + Přepnout na číslovaný seznam + V této místnosti nejsou žádné předchozí hlasování + Předchozí hlasování + V této místnosti nejsou žádné aktivní hlasování + Aktivní hlasování + Historie hlasování \ No newline at end of file From 6cdd8096cd0889c68b2b68f792cf251c4ca40243 Mon Sep 17 00:00:00 2001 From: Christian Paul Date: Mon, 9 Jan 2023 09:39:16 +0000 Subject: [PATCH 45/75] Translated using Weblate (Esperanto) Currently translated at 75.8% (1961 of 2585 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/eo/ --- library/ui-strings/src/main/res/values-eo/strings.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/library/ui-strings/src/main/res/values-eo/strings.xml b/library/ui-strings/src/main/res/values-eo/strings.xml index 4521e840a6..e417d183bf 100644 --- a/library/ui-strings/src/main/res/values-eo/strings.xml +++ b/library/ui-strings/src/main/res/values-eo/strings.xml @@ -1123,9 +1123,7 @@ Elektu landon Administri retpoŝtadresojn kaj telefonnumerojn ligitajn al via konto de Matrix Retpoŝtadresoj kaj telefonnumeroj - Ĉu montri ĉiujn mesaĝojn de %s\? -\n -\nSciu ke tiu ĉi ago reekigos la aplikaĵon, kaj tio povas daŭri iom da tempo. + Ĉu montri ĉiujn mesaĝojn de %s\? Via pasvorto ĝisdatiĝis La pasvorto ne validas Malsukcesis ĝisdatigi pasvorton @@ -2201,4 +2199,5 @@ Sonorante… Aroj - Iom uzantoj reatentita + \@room \ No newline at end of file From 07db45a1678d8cc7d989008b643f867389c602c6 Mon Sep 17 00:00:00 2001 From: Szimszon Date: Sat, 7 Jan 2023 20:46:31 +0000 Subject: [PATCH 46/75] Translated using Weblate (Hungarian) Currently translated at 100.0% (2585 of 2585 strings) Translation: Element Android/Element Android App Translate-URL: https://translate.element.io/projects/element-android/element-app/hu/ --- library/ui-strings/src/main/res/values-hu/strings.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/library/ui-strings/src/main/res/values-hu/strings.xml b/library/ui-strings/src/main/res/values-hu/strings.xml index 1be136bb39..9fdad2dbf0 100644 --- a/library/ui-strings/src/main/res/values-hu/strings.xml +++ b/library/ui-strings/src/main/res/values-hu/strings.xml @@ -2890,4 +2890,13 @@ A Visszaállítási Kulcsot tartsd biztonságos helyen, mint pl. egy jelszókeze Hivatkozás Szöveg Hivatkozás beállítása + A hozzáférési kulcs teljes elérést biztosít a fiókhoz. Soha ne ossza meg mással. + Elérési kulcs + Lista ki-,bekapcsolása + Számozott lista ki-,bekapcsolása + Nincsenek régi szavazások ebben a szobában + Régi szavazások + Nincsenek aktív szavazások ebben a szobában + Aktív szavazások + Szavazás alakulása \ No newline at end of file From 6813571015c06959753d1aba647e12d2914e1c7e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 12 Jan 2023 18:46:18 +0300 Subject: [PATCH 47/75] Fix rendering bug when poll is edited from another client. --- .../internal/session/room/EventEditValidator.kt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt index 41d0c3f6ab..5a66e7e62d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/EventEditValidator.kt @@ -16,13 +16,16 @@ package org.matrix.android.sdk.internal.session.room +import org.matrix.android.sdk.api.session.events.model.Content 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.LocalEcho 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.toValidDecryptedEvent import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import timber.log.Timber import javax.inject.Inject @@ -101,7 +104,7 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto if (originalDecrypted.type != replaceDecrypted.type) { return EditValidity.Invalid("replacement and original events must have the same type") } - if (replaceDecrypted.clearContent.toModel()?.newContent == null) { + if (!hasNewContent(replaceDecrypted.type, replaceDecrypted.clearContent)) { return EditValidity.Invalid("replacement event must have an m.new_content property") } } else { @@ -116,11 +119,18 @@ internal class EventEditValidator @Inject constructor(val cryptoStore: IMXCrypto if (originalEvent.type != replaceEvent.type) { return EditValidity.Invalid("replacement and original events must have the same type") } - if (replaceEvent.content.toModel()?.newContent == null) { + if (!hasNewContent(replaceEvent.type, replaceEvent.content)) { return EditValidity.Invalid("replacement event must have an m.new_content property") } } return EditValidity.Valid } + + private fun hasNewContent(eventType: String?, content: Content?): Boolean { + return when (eventType) { + in EventType.POLL_START.values -> content.toModel()?.newContent != null + else -> content.toModel()?.newContent != null + } + } } From 72e0dc4bd2c366b8dd7a30513900c5736b6075c3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 12 Jan 2023 16:13:34 +0100 Subject: [PATCH 48/75] Voice Broadcast - only send a notification on the first chunk --- changelog.d/7845.wip | 1 + .../src/main/res/values/strings.xml | 1 + .../format/DisplayableEventFormatter.kt | 16 +++-- .../notifications/FilteredEventDetector.kt | 59 +++++++++++++++++++ .../NotificationDrawerManager.kt | 6 ++ 5 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 changelog.d/7845.wip create mode 100644 vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt diff --git a/changelog.d/7845.wip b/changelog.d/7845.wip new file mode 100644 index 0000000000..8bce21499a --- /dev/null +++ b/changelog.d/7845.wip @@ -0,0 +1 @@ +[Voice Broadcast] Only display a notification on the first voice chunk diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f1c2eb2c6d..6731248f83 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -2295,6 +2295,7 @@ Verification Conclusion Shared their location Shared their live location + Started a voice broadcast Waiting… %s canceled diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt index 5fa9576dd4..f57aa67800 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt @@ -27,6 +27,7 @@ import im.vector.app.core.resources.StringProvider import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.voicebroadcast.VoiceBroadcastConstants import im.vector.app.features.voicebroadcast.isLive +import im.vector.app.features.voicebroadcast.isVoiceBroadcast import im.vector.app.features.voicebroadcast.model.asVoiceBroadcastEvent import me.gujun.android.span.image import me.gujun.android.span.span @@ -39,6 +40,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageType +import org.matrix.android.sdk.api.session.room.model.message.asMessageAudioEvent import org.matrix.android.sdk.api.session.room.model.relation.ReactionContent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getTextDisplayableContent @@ -86,10 +88,16 @@ class DisplayableEventFormatter @Inject constructor( simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor) } MessageType.MSGTYPE_AUDIO -> { - if ((messageContent as? MessageAudioContent)?.voiceMessageIndicator != null) { - simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor) - } else { - simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor) + when { + (messageContent as? MessageAudioContent)?.voiceMessageIndicator == null -> { + simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor) + } + timelineEvent.root.asMessageAudioEvent().isVoiceBroadcast() -> { + simpleFormat(senderName, stringProvider.getString(R.string.started_a_voice_broadcast), appendAuthor) + } + else -> { + simpleFormat(senderName, stringProvider.getString(R.string.sent_a_voice_message), appendAuthor) + } } } MessageType.MSGTYPE_VIDEO -> { diff --git a/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt b/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt new file mode 100644 index 0000000000..e21462b182 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/notifications/FilteredEventDetector.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023 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.notifications + +import im.vector.app.ActiveSessionDataSource +import im.vector.app.features.voicebroadcast.isVoiceBroadcast +import im.vector.app.features.voicebroadcast.sequence +import org.matrix.android.sdk.api.session.events.model.isVoiceMessage +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.model.message.asMessageAudioEvent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import javax.inject.Inject + +class FilteredEventDetector @Inject constructor( + private val activeSessionDataSource: ActiveSessionDataSource +) { + + /** + * Returns true if the given event should be ignored. + * Used to skip notifications if a non expected message is received. + */ + fun shouldBeIgnored(notifiableEvent: NotifiableEvent): Boolean { + val session = activeSessionDataSource.currentValue?.orNull() ?: return false + + if (notifiableEvent is NotifiableMessageEvent) { + val room = session.getRoom(notifiableEvent.roomId) ?: return false + val timelineEvent = room.getTimelineEvent(notifiableEvent.eventId) ?: return false + return timelineEvent.shouldBeIgnored() + } + return false + } + + /** + * Whether the timeline event should be ignored. + */ + private fun TimelineEvent.shouldBeIgnored(): Boolean { + if (root.isVoiceMessage()) { + val audioEvent = root.asMessageAudioEvent() + // if the event is a voice message related to a voice broadcast, only show the event on the first chunk. + return audioEvent.isVoiceBroadcast() && audioEvent?.sequence != 1 + } + + return false + } +} diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt index 4f05e83bd4..2d799034d9 100644 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationDrawerManager.kt @@ -47,6 +47,7 @@ class NotificationDrawerManager @Inject constructor( private val notifiableEventProcessor: NotifiableEventProcessor, private val notificationRenderer: NotificationRenderer, private val notificationEventPersistence: NotificationEventPersistence, + private val filteredEventDetector: FilteredEventDetector, private val buildMeta: BuildMeta, ) { @@ -100,6 +101,11 @@ class NotificationDrawerManager @Inject constructor( Timber.d("onNotifiableEventReceived(): is push: ${notifiableEvent.canBeReplaced}") } + if (filteredEventDetector.shouldBeIgnored(notifiableEvent)) { + Timber.d("onNotifiableEventReceived(): ignore the event") + return + } + add(notifiableEvent) } From 8a2f28bc376fd2f34c97270cdff7336332efe50e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 12 Jan 2023 18:29:56 +0100 Subject: [PATCH 49/75] Add comment to explain the error handling --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index ab4b6c2269..1cb418163f 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -282,6 +282,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( } } catch (failure: VoiceBroadcastFailure.ListeningError.DownloadError) { isPreparingNextPlayer = false + // Do not change the playingState if the current player is still valid, + // the error will be thrown again when switching to the next player if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) { playingState = State.Error(failure) } @@ -453,6 +455,8 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onError(mp: MediaPlayer, what: Int, extra: Int): Boolean { Timber.d("## Voice Broadcast | onError: what=$what, extra=$extra") + // Do not change the playingState if the current player is still valid, + // the error will be thrown again when switching to the next player if (playingState == State.Buffering || tryOrNull { currentMediaPlayer?.isPlaying } != true) { playingState = State.Error(VoiceBroadcastFailure.ListeningError.UnableToPlay(what, extra)) } From d55f1efd632d2426ba1ab2e0841b73d0948e8d60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 Jan 2023 00:17:34 +0000 Subject: [PATCH 50/75] Bump wysiwyg from 0.15.0 to 0.17.0 Bumps [wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 0.15.0 to 0.17.0. - [Release notes](https://github.com/matrix-org/matrix-wysiwyg/releases) - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/0.15.0...0.17.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index 8b0933b943..f9d2850e58 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -103,7 +103,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.15.0" + 'wysiwyg' : "io.element.android:wysiwyg:0.17.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From cad5e732dfb6af7831017bdfb44792138da0521a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Jan 2023 10:36:03 +0100 Subject: [PATCH 51/75] Fix issue of send button not displayed when starting message with a space. --- .../home/room/detail/composer/MessageComposerViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt index c02eb1fa8a..56ee9ffb5a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/composer/MessageComposerViewModel.kt @@ -138,7 +138,7 @@ class MessageComposerViewModel @AssistedInject constructor( } private fun handleOnTextChanged(action: MessageComposerAction.OnTextChanged) { - val needsSendButtonVisibilityUpdate = currentComposerText.isEmpty() != action.text.isEmpty() + val needsSendButtonVisibilityUpdate = currentComposerText.isBlank() != action.text.isBlank() currentComposerText = SpannableString(action.text) if (needsSendButtonVisibilityUpdate) { updateIsSendButtonVisibility(true) From 06ac22488e7f2d93d0c608ff8bbee632dfe3129f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 13 Jan 2023 10:45:09 +0100 Subject: [PATCH 52/75] Fix typo. --- library/ui-strings/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index f1c2eb2c6d..405dae0e16 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -794,7 +794,7 @@ Shows all threads you’ve participated in Keep discussions organized with threads Threads help keep your conversations on-topic and easy to track. - You\'re homeserver does not support listing threads yet. + Your homeserver does not support listing threads yet. Tip: Long tap a message and use “%s”. From a Thread From e5801a4f19a00fd76755c898be0a5a041f343aac Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 13 Jan 2023 15:56:38 +0300 Subject: [PATCH 53/75] Make verification dialog cancelable. --- .../crypto/verification/VerificationBottomSheet.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt index 38b72f2022..349dfc738d 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt @@ -17,6 +17,7 @@ package im.vector.app.features.crypto.verification import android.app.Activity import android.app.Dialog +import android.content.DialogInterface import android.os.Bundle import android.os.Parcelable import android.view.KeyEvent @@ -85,7 +86,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment Date: Fri, 13 Jan 2023 15:56:47 +0300 Subject: [PATCH 54/75] Add changelog. --- changelog.d/4025.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/4025.bugfix diff --git a/changelog.d/4025.bugfix b/changelog.d/4025.bugfix new file mode 100644 index 0000000000..109da1c830 --- /dev/null +++ b/changelog.d/4025.bugfix @@ -0,0 +1 @@ +Fix can't get out of a verification dialog From 169c9b221ca69039406a50744749c5336c4af85a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 13 Jan 2023 15:46:48 +0100 Subject: [PATCH 55/75] Throw an error if the media player which has completed is not the expected one --- .../voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt index 1cb418163f..2e1600e4e2 100644 --- a/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt +++ b/vector/src/main/java/im/vector/app/features/voicebroadcast/listening/VoiceBroadcastPlayerImpl.kt @@ -436,7 +436,11 @@ class VoiceBroadcastPlayerImpl @Inject constructor( override fun onCompletion(mp: MediaPlayer) { // Release media player as soon as it completed mp.release() - currentMediaPlayer = null + if (currentMediaPlayer == mp) { + currentMediaPlayer = null + } else { + error("The media player which has completed mismatches the current media player instance.") + } // Next media player is already attached to this player and will start playing automatically if (nextMediaPlayer != null) return From 4a49f2ff9b6acda4cd879240034884d15ed9e31d Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 13 Jan 2023 18:15:52 +0300 Subject: [PATCH 56/75] Check if bottom sheet is cancellable. --- .../features/crypto/verification/VerificationBottomSheet.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt index 349dfc738d..3b9de57be8 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt @@ -85,10 +85,6 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment Date: Fri, 13 Jan 2023 16:48:53 +0100 Subject: [PATCH 57/75] Check encrypted event status using the Event model --- .../database/EventInsertLiveObserver.kt | 51 ++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt index 2cae6c09ed..a3f38cf2c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/EventInsertLiveObserver.kt @@ -55,32 +55,46 @@ internal class EventInsertLiveObserver @Inject constructor( if (!results.isLoaded || results.isEmpty()) { return@withLock } - val idsToDeleteAfterProcess = ArrayList() - val idsOfEncryptedEvents = ArrayList() - val filteredEvents = ArrayList(results.size) + val eventsToProcess = ArrayList(results.size) + val eventsToIgnore = ArrayList(results.size) + Timber.v("EventInsertEntity updated with ${results.size} results in db") results.forEach { - if (shouldProcess(it)) { - // don't use copy from realm over there - val copiedEvent = EventInsertEntity( - eventId = it.eventId, - eventType = it.eventType - ).apply { - insertType = it.insertType - } - filteredEvents.add(copiedEvent) + // don't use copy from realm over there + val copiedEvent = EventInsertEntity( + eventId = it.eventId, + eventType = it.eventType + ).apply { + insertType = it.insertType } - if (it.eventType == EventType.ENCRYPTED) { - idsOfEncryptedEvents.add(it.eventId) + + if (shouldProcess(it)) { + eventsToProcess.add(copiedEvent) } else { - idsToDeleteAfterProcess.add(it.eventId) + eventsToIgnore.add(copiedEvent) } } + awaitTransaction(realmConfiguration) { realm -> - Timber.v("##Transaction: There are ${filteredEvents.size} events to process ") - filteredEvents.forEach { eventInsert -> + Timber.v("##Transaction: There are ${eventsToProcess.size} events to process") + + val idsToDeleteAfterProcess = ArrayList() + val idsOfEncryptedEvents = ArrayList() + val getAndTriageEvent: (EventInsertEntity) -> Event? = { eventInsert -> val eventId = eventInsert.eventId val event = getEvent(realm, eventId) + if (event?.getClearType() == EventType.ENCRYPTED) { + idsOfEncryptedEvents.add(eventId) + } else { + idsToDeleteAfterProcess.add(eventId) + } + event + } + + eventsToProcess.forEach { eventInsert -> + val eventId = eventInsert.eventId + val event = getAndTriageEvent(eventInsert) + if (event != null && canProcessEvent(event)) { processors.filter { it.shouldProcess(eventId, event.getClearType(), eventInsert.insertType) @@ -92,6 +106,9 @@ internal class EventInsertLiveObserver @Inject constructor( return@forEach } } + + eventsToIgnore.forEach { getAndTriageEvent(it) } + realm.where(EventInsertEntity::class.java) .`in`(EventInsertEntityFields.EVENT_ID, idsToDeleteAfterProcess.toTypedArray()) .findAll() From a586e346dedad6e48c7780a0b120cc0bcab361c0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 16 Jan 2023 13:52:15 +0100 Subject: [PATCH 58/75] Fix an issue on Breadcrumbs in dark theme on the draft indicator --- vector/src/main/res/layout/item_breadcrumbs.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/res/layout/item_breadcrumbs.xml b/vector/src/main/res/layout/item_breadcrumbs.xml index 7584d6f9d5..83624036d9 100644 --- a/vector/src/main/res/layout/item_breadcrumbs.xml +++ b/vector/src/main/res/layout/item_breadcrumbs.xml @@ -85,6 +85,7 @@ app:layout_constraintCircle="@id/breadcrumbsImageView" app:layout_constraintCircleAngle="225" app:layout_constraintCircleRadius="28dp" + app:tint="?vctr_content_primary" tools:ignore="MissingConstraints" tools:visibility="visible" /> From a8d2f40475616269f4669e74589803510b027055 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 23:02:58 +0000 Subject: [PATCH 59/75] Bump flipper from 0.176.1 to 0.177.0 Bumps `flipper` from 0.176.1 to 0.177.0. Updates `flipper` from 0.176.1 to 0.177.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.176.1...v0.177.0) Updates `flipper-network-plugin` from 0.176.1 to 0.177.0 - [Release notes](https://github.com/facebook/flipper/releases) - [Commits](https://github.com/facebook/flipper/compare/v0.176.1...v0.177.0) --- updated-dependencies: - dependency-name: com.facebook.flipper:flipper dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: com.facebook.flipper:flipper-network-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index f6be35edc0..ff94b18943 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -18,7 +18,7 @@ def markwon = "4.6.2" def moshi = "1.14.0" def lifecycle = "2.5.1" def flowBinding = "1.2.0" -def flipper = "0.176.1" +def flipper = "0.177.0" def epoxy = "5.0.0" def mavericks = "3.0.1" def glide = "4.14.2" From bc4f1f1ec054c382e9f6355ec7b4279105ee046e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Jan 2023 23:03:48 +0000 Subject: [PATCH 60/75] Bump wysiwyg from 0.17.0 to 0.18.0 Bumps [wysiwyg](https://github.com/matrix-org/matrix-wysiwyg) from 0.17.0 to 0.18.0. - [Release notes](https://github.com/matrix-org/matrix-wysiwyg/releases) - [Changelog](https://github.com/matrix-org/matrix-rich-text-editor/blob/main/CHANGELOG.md) - [Commits](https://github.com/matrix-org/matrix-wysiwyg/compare/0.17.0...0.18.0) --- updated-dependencies: - dependency-name: io.element.android:wysiwyg dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index f6be35edc0..03873f8d25 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -103,7 +103,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.17.0" + 'wysiwyg' : "io.element.android:wysiwyg:0.18.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", From 479b573dbbb50467443b49e1acde79910e40897e Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 11 Jan 2023 11:08:32 +0100 Subject: [PATCH 61/75] Adding changelog entry --- changelog.d/7864.wip | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/7864.wip diff --git a/changelog.d/7864.wip b/changelog.d/7864.wip new file mode 100644 index 0000000000..703318ba2e --- /dev/null +++ b/changelog.d/7864.wip @@ -0,0 +1 @@ +[Poll] Load more UI mechanism From e8e94b51896d54ef0bf94160f93940a8c1ed6890 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL <46314705+mnaturel@users.noreply.github.com> Date: Wed, 11 Jan 2023 14:41:25 +0100 Subject: [PATCH 62/75] Adding load more item at the end of the list of polls --- .../src/main/res/values/strings.xml | 1 + .../polls/list/RoomPollLoadMoreItem.kt | 42 +++++++++++++++++++ .../polls/list/RoomPollsController.kt | 11 +++++ .../polls/list/RoomPollsListFragment.kt | 6 +++ .../main/res/layout/item_poll_load_more.xml | 29 +++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollLoadMoreItem.kt create mode 100644 vector/src/main/res/layout/item_poll_load_more.xml diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml index 9fd121b3f7..f6e2491674 100644 --- a/library/ui-strings/src/main/res/values/strings.xml +++ b/library/ui-strings/src/main/res/values/strings.xml @@ -3201,6 +3201,7 @@ There are no active polls in this room Past polls There are no past polls in this room + Load more polls Share location diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollLoadMoreItem.kt b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollLoadMoreItem.kt new file mode 100644 index 0000000000..a684fa5c9e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/polls/list/RoomPollLoadMoreItem.kt @@ -0,0 +1,42 @@ +/* + * 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.roomprofile.polls.list + +import android.widget.Button +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick + +@EpoxyModelClass +abstract class RoomPollLoadMoreItem : VectorEpoxyModel(R.layout.item_poll_load_more) { + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var clickListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.loadMoreButton.onClick(clickListener) + } + + class Holder : VectorEpoxyHolder() { + val loadMoreButton by bind