diff --git a/changelog.d/6437.feature b/changelog.d/6437.feature new file mode 100644 index 0000000000..fb24819daf --- /dev/null +++ b/changelog.d/6437.feature @@ -0,0 +1 @@ +[Location sharing] - Delete action on a live message diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt index 554dc2ec9d..59dc6c434d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt @@ -371,6 +371,8 @@ fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClear fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER +fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO + fun Event.getRelationContent(): RelationDefaultContent? { return if (isEncrypted()) { content.toModel()?.relatesTo diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 14095b67c0..cd8acbcccc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.api.session.room.location -import androidx.annotation.MainThread import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable @@ -59,16 +58,21 @@ interface LocationSharingService { */ suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult + /** + * Redact (delete) the live associated to the given beacon info event id. + * @param beaconInfoEventId event id of the initial beacon info state event + * @param reason Optional reason string + */ + suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) + /** * Returns a LiveData on the list of current running live location shares. */ - @MainThread fun getRunningLiveLocationShareSummaries(): LiveData> /** * Returns a LiveData on the live location share summary with the given eventId. * @param beaconInfoEventId event id of the initial beacon info state event */ - @MainThread fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData> } 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 9d8c8a13bd..d391abf1e6 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 @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.getRelationContent import org.matrix.android.sdk.api.session.events.model.isEdition +import org.matrix.android.sdk.api.session.events.model.isLiveLocation import org.matrix.android.sdk.api.session.events.model.isPoll import org.matrix.android.sdk.api.session.events.model.isReply import org.matrix.android.sdk.api.session.events.model.isSticker @@ -165,6 +166,10 @@ fun TimelineEvent.isSticker(): Boolean { return root.isSticker() } +fun TimelineEvent.isLiveLocation(): Boolean { + return root.isLiveLocation() +} + /** * Returns whether or not the event is a root thread event. */ 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 b54aec26b2..9784412761 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 @@ -49,6 +49,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032 +import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033 import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import javax.inject.Inject @@ -57,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor( private val normalizer: Normalizer ) : MatrixRealmMigration( dbName = "Session", - schemaVersion = 32L, + schemaVersion = 33L, ) { /** * Forces all RealmSessionStoreMigration instances to be equal. @@ -99,5 +100,6 @@ internal class RealmSessionStoreMigration @Inject constructor( if (oldVersion < 30) MigrateSessionTo030(realm).perform() if (oldVersion < 31) MigrateSessionTo031(realm).perform() if (oldVersion < 32) MigrateSessionTo032(realm).perform() + if (oldVersion < 33) MigrateSessionTo033(realm).perform() } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt new file mode 100644 index 0000000000..0e3a8599c5 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo033.kt @@ -0,0 +1,33 @@ +/* + * 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.migration + +import io.realm.DynamicRealm +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.internal.util.database.RealmMigrator + +/** + * Migrating to: + * Live location sharing aggregated summary: adding new field relatedEventIds. + */ +internal class MigrateSessionTo033(realm: DynamicRealm) : RealmMigrator(realm, 33) { + + override fun doMigrate(realm: DynamicRealm) { + realm.schema.get("LiveLocationShareAggregatedSummaryEntity") + ?.addRealmListField(LiveLocationShareAggregatedSummaryEntityFields.RELATED_EVENT_IDS.`$`, String::class.java) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt index c5df8e9338..08ea06bb1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/livelocation/LiveLocationShareAggregatedSummaryEntity.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.internal.database.model.livelocation +import io.realm.RealmList import io.realm.RealmObject import io.realm.annotations.PrimaryKey @@ -29,6 +30,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity( @PrimaryKey var eventId: String = "", + /** + * List of event ids used to compute the aggregated summary data. + */ + var relatedEventIds: RealmList = RealmList(), + var roomId: String = "", var userId: String = "", diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt index 6caa832110..1c19c21de2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/EventAnnotationsSummaryEntityQuery.kt @@ -23,6 +23,11 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery { + return realm.where() + .equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId) +} + internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { return realm.where() .equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId) @@ -44,3 +49,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, r return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst() ?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId) } + +internal fun EventAnnotationsSummaryEntity.Companion.get(realm: Realm, eventId: String): EventAnnotationsSummaryEntity? { + return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index d69f251f6f..fbf7e963a7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -23,6 +23,14 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( + realm: Realm, + eventId: String, +): RealmQuery { + return realm.where() + .equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) +} + internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where( realm: Realm, roomId: String, @@ -72,6 +80,13 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst() } +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get( + realm: Realm, + eventId: String, +): LiveLocationShareAggregatedSummaryEntity? { + return LiveLocationShareAggregatedSummaryEntity.where(realm, eventId).findFirst() +} + internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser( realm: Realm, roomId: String, 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 f8a52f0b7e..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 @@ -88,6 +88,7 @@ import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationPro import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor +import org.matrix.android.sdk.internal.session.room.location.LiveLocationShareRedactionEventProcessor import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine @@ -321,6 +322,10 @@ internal abstract class SessionModule { @IntoSet abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor + @Binds + @IntoSet + abstract fun bindLiveLocationShareRedactionEventProcessor(processor: LiveLocationShareRedactionEventProcessor): EventInsertLiveProcessor + @Binds @IntoSet abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index e1dd22a211..d01324a35f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -58,11 +58,13 @@ import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVi import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask +import org.matrix.android.sdk.internal.session.room.location.DefaultRedactLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask +import org.matrix.android.sdk.internal.session.room.location.RedactLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask @@ -339,4 +341,7 @@ internal abstract class RoomModule { @Binds abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask + + @Binds + abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt index 921749122b..3f5b1e1360 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessor.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import androidx.work.ExistingWorkPolicy import io.realm.Realm +import io.realm.RealmList import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toContent @@ -73,6 +74,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor( eventId = targetEventId ) + if (!isLive && !event.eventId.isNullOrEmpty()) { + // in this case, the received event is a new state event related to the previous one + addRelatedEventId(event.eventId, aggregatedSummary) + } + // remote event can stay with isLive == true while the local summary is no more active val isActive = aggregatedSummary.isActive.orTrue() && isLive val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) } @@ -144,6 +150,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor( roomId = roomId, eventId = relatedEventId ) + + if (!event.eventId.isNullOrEmpty()) { + addRelatedEventId(event.eventId, aggregatedSummary) + } + val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0 val currentLocationTimestamp = ContentMapper .map(aggregatedSummary.lastLocationContent) @@ -160,6 +171,17 @@ internal class LiveLocationAggregationProcessor @Inject constructor( } } + private fun addRelatedEventId( + eventId: String, + aggregatedSummary: LiveLocationShareAggregatedSummaryEntity + ) { + Timber.d("adding related event id $eventId to summary of id ${aggregatedSummary.eventId}") + val updatedEventIds = aggregatedSummary.relatedEventIds.toMutableList().also { + it.add(eventId) + } + aggregatedSummary.relatedEventIds = RealmList(*updatedEventIds.toTypedArray()) + } + private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) { LiveLocationShareAggregatedSummaryEntity .findActiveLiveInRoomForUser( diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index a8a9691ce9..60312071d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -42,6 +42,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( private val startLiveLocationShareTask: StartLiveLocationShareTask, private val stopLiveLocationShareTask: StopLiveLocationShareTask, private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask, + private val redactLiveLocationShareTask: RedactLiveLocationShareTask, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, ) : LocationSharingService { @@ -102,6 +103,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return stopLiveLocationShareTask.execute(params) } + override suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) { + val params = RedactLiveLocationShareTask.Params( + roomId = roomId, + beaconInfoEventId = beaconInfoEventId, + reason = reason + ) + return redactLiveLocationShareTask.execute(params) + } + override fun getRunningLiveLocationShareSummaries(): LiveData> { return monarchy.findAllMappedWithChanges( { LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) }, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt new file mode 100644 index 0000000000..fa3479ed3c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessor.kt @@ -0,0 +1,65 @@ +/* + * 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.room.location + +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.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.model.EventInsertType +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor +import timber.log.Timber +import javax.inject.Inject + +/** + * Listens to the database for the insertion of any redaction event. + * Delete specifically the aggregated summary related to a redacted live location share event. + */ +internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor { + + override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean { + return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO + } + + override suspend fun process(realm: Realm, event: Event) { + if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) { + return + } + + val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst() + ?: return + + if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) { + val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId) + + if (liveSummary != null) { + Timber.d("deleting live summary with id: ${liveSummary.eventId}") + liveSummary.deleteFromRealm() + val annotationsSummary = EventAnnotationsSummaryEntity.get(realm, eventId = redactedEvent.eventId) + if (annotationsSummary != null) { + Timber.d("deleting annotation summary with id: ${annotationsSummary.eventId}") + annotationsSummary.deleteFromRealm() + } + } + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt new file mode 100644 index 0000000000..ac855b81e7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/RedactLiveLocationShareTask.kt @@ -0,0 +1,78 @@ +/* + * 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.location + +import io.realm.RealmConfiguration +import org.matrix.android.sdk.internal.database.awaitTransaction +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.get +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor +import org.matrix.android.sdk.internal.task.Task +import timber.log.Timber +import javax.inject.Inject + +internal interface RedactLiveLocationShareTask : Task { + data class Params( + val roomId: String, + val beaconInfoEventId: String, + val reason: String? + ) +} + +internal class DefaultRedactLiveLocationShareTask @Inject constructor( + @SessionDatabase private val realmConfiguration: RealmConfiguration, + private val localEchoEventFactory: LocalEchoEventFactory, + private val eventSenderProcessor: EventSenderProcessor, +) : RedactLiveLocationShareTask { + + override suspend fun execute(params: RedactLiveLocationShareTask.Params) { + val relatedEventIds = getRelatedEventIdsOfLive(params.beaconInfoEventId) + Timber.d("beacon with id ${params.beaconInfoEventId} has related event ids: ${relatedEventIds.joinToString(", ")}") + + postRedactionWithLocalEcho( + eventId = params.beaconInfoEventId, + roomId = params.roomId, + reason = params.reason + ) + relatedEventIds.forEach { eventId -> + postRedactionWithLocalEcho( + eventId = eventId, + roomId = params.roomId, + reason = params.reason + ) + } + } + + private suspend fun getRelatedEventIdsOfLive(beaconInfoEventId: String): List { + return awaitTransaction(realmConfiguration) { realm -> + val aggregatedSummaryEntity = LiveLocationShareAggregatedSummaryEntity.get( + realm = realm, + eventId = beaconInfoEventId + ) + aggregatedSummaryEntity?.relatedEventIds?.toList() ?: emptyList() + } + } + + private fun postRedactionWithLocalEcho(eventId: String, roomId: String, reason: String?) { + Timber.d("posting redaction for event of id $eventId") + val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, eventId, reason) + localEchoEventFactory.createLocalEcho(redactionEcho) + eventSenderProcessor.postRedaction(redactionEcho, reason) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt index e33fbb56b1..cc86679cbc 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/prune/RedactionEventProcessor.kt @@ -74,6 +74,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr when (typeToPrune) { EventType.ENCRYPTED, EventType.MESSAGE, + in EventType.STATE_ROOM_BEACON_INFO, + in EventType.BEACON_LOCATION_DATA, in EventType.POLL_START -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") val unsignedData = EventMapper.map(eventToPrune).unsignedData diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index 933087af2b..a5e91714b7 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation import androidx.work.ExistingWorkPolicy import org.amshove.kluent.shouldBeEqualTo +import org.amshove.kluent.shouldContain import org.junit.Test import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.UnsignedData @@ -199,9 +200,10 @@ internal class LiveLocationAggregationProcessorTest { age = 123, replacesState = AN_EVENT_ID ) + val stateEventId = "state-event-id" val event = Event( senderId = A_SENDER_ID, - eventId = "", + eventId = stateEventId, unsignedData = unsignedData ) val beaconInfo = MessageBeaconInfoContent( @@ -237,6 +239,7 @@ internal class LiveLocationAggregationProcessorTest { aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID aggregatedEntity.isActive shouldBeEqualTo false + aggregatedEntity.relatedEventIds shouldContain stateEventId aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS aggregatedEntity.lastLocationContent shouldBeEqualTo null previousEntities.forEach { entity -> @@ -324,7 +327,7 @@ internal class LiveLocationAggregationProcessorTest { val lastBeaconLocationContent = MessageBeaconLocationDataContent( unstableTimestampMillis = A_TIMESTAMP ) - givenLastSummaryQueryReturns( + val aggregatedEntity = givenLastSummaryQueryReturns( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, beaconLocationContent = lastBeaconLocationContent @@ -340,6 +343,7 @@ internal class LiveLocationAggregationProcessorTest { ) result shouldBeEqualTo false + aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID } @Test @@ -353,7 +357,7 @@ internal class LiveLocationAggregationProcessorTest { val lastBeaconLocationContent = MessageBeaconLocationDataContent( unstableTimestampMillis = A_TIMESTAMP - 60_000 ) - val entity = givenLastSummaryQueryReturns( + val aggregatedEntity = givenLastSummaryQueryReturns( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, beaconLocationContent = lastBeaconLocationContent @@ -369,7 +373,8 @@ internal class LiveLocationAggregationProcessorTest { ) result shouldBeEqualTo true - val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel() + aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID + val savedLocationData = ContentMapper.map(aggregatedEntity.lastLocationContent).toModel() savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index ef9bde2c49..a01f51604c 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -22,8 +22,10 @@ import androidx.lifecycle.Transformations import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.runs import io.mockk.slot import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -52,6 +54,7 @@ private const val A_LONGITUDE = 40.0 private const val AN_UNCERTAINTY = 5.0 private const val A_TIMEOUT = 15_000L private const val A_DESCRIPTION = "description" +private const val A_REASON = "reason" @ExperimentalCoroutinesApi internal class DefaultLocationSharingServiceTest { @@ -62,6 +65,7 @@ internal class DefaultLocationSharingServiceTest { private val startLiveLocationShareTask = mockk() private val stopLiveLocationShareTask = mockk() private val checkIfExistingActiveLiveTask = mockk() + private val redactLiveLocationShareTask = mockk() private val fakeLiveLocationShareAggregatedSummaryMapper = mockk() private val defaultLocationSharingService = DefaultLocationSharingService( @@ -72,6 +76,7 @@ internal class DefaultLocationSharingServiceTest { startLiveLocationShareTask = startLiveLocationShareTask, stopLiveLocationShareTask = stopLiveLocationShareTask, checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask, + redactLiveLocationShareTask = redactLiveLocationShareTask, liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper ) @@ -209,6 +214,20 @@ internal class DefaultLocationSharingServiceTest { coVerify { stopLiveLocationShareTask.execute(expectedParams) } } + @Test + fun `live location share can be redacted`() = runTest { + coEvery { redactLiveLocationShareTask.execute(any()) } just runs + + defaultLocationSharingService.redactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON) + + val expectedParams = RedactLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + beaconInfoEventId = AN_EVENT_ID, + reason = A_REASON + ) + coVerify { redactLiveLocationShareTask.execute(expectedParams) } + } + @Test fun `livedata of live summaries is correctly computed`() { val entity = LiveLocationShareAggregatedSummaryEntity() diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt new file mode 100644 index 0000000000..b8618d1a79 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultRedactLiveLocationShareTaskTest.kt @@ -0,0 +1,126 @@ +/* + * 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.location + +import io.mockk.unmockkAll +import io.realm.RealmList +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor +import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.FakeRealmConfiguration +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val A_ROOM_ID = "room-id" +private const val AN_EVENT_ID = "event-id" +private const val AN_EVENT_ID_1 = "event-id-1" +private const val AN_EVENT_ID_2 = "event-id-2" +private const val AN_EVENT_ID_3 = "event-id-3" +private const val A_REASON = "reason" + +@ExperimentalCoroutinesApi +class DefaultRedactLiveLocationShareTaskTest { + + private val fakeRealmConfiguration = FakeRealmConfiguration() + private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory() + private val fakeEventSenderProcessor = FakeEventSenderProcessor() + private val fakeRealm = FakeRealm() + + private val defaultRedactLiveLocationShareTask = DefaultRedactLiveLocationShareTask( + realmConfiguration = fakeRealmConfiguration.instance, + localEchoEventFactory = fakeLocalEchoEventFactory.instance, + eventSenderProcessor = fakeEventSenderProcessor + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given parameters when redacting then post redact events and related and creates redact local echos`() = runTest { + val params = createParams() + val relatedEventIds = listOf(AN_EVENT_ID_1, AN_EVENT_ID_2, AN_EVENT_ID_3) + val aggregatedSummaryEntity = createSummary(relatedEventIds) + givenSummaryForId(AN_EVENT_ID, aggregatedSummaryEntity) + fakeRealmConfiguration.givenAwaitTransaction>(fakeRealm.instance) + val redactEvents = givenCreateRedactEventWithLocalEcho(relatedEventIds + AN_EVENT_ID) + givenPostRedaction(redactEvents) + + defaultRedactLiveLocationShareTask.execute(params) + + verifyCreateRedactEventForEventIds(relatedEventIds + AN_EVENT_ID) + verifyCreateLocalEchoForEvents(redactEvents) + } + + private fun createParams() = RedactLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + beaconInfoEventId = AN_EVENT_ID, + reason = A_REASON + ) + + private fun createSummary(relatedEventIds: List): LiveLocationShareAggregatedSummaryEntity { + return LiveLocationShareAggregatedSummaryEntity( + eventId = AN_EVENT_ID, + relatedEventIds = RealmList(*relatedEventIds.toTypedArray()), + ) + } + + private fun givenSummaryForId(eventId: String, aggregatedSummaryEntity: LiveLocationShareAggregatedSummaryEntity) { + fakeRealm.givenWhere() + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId) + .givenFindFirst(aggregatedSummaryEntity) + } + + private fun givenCreateRedactEventWithLocalEcho(eventIds: List): List { + return eventIds.map { eventId -> + fakeLocalEchoEventFactory.givenCreateRedactEvent( + eventId = eventId, + withLocalEcho = true + ) + } + } + + private fun givenPostRedaction(redactEvents: List) { + redactEvents.forEach { + fakeEventSenderProcessor.givenPostRedaction(event = it, reason = A_REASON) + } + } + + private fun verifyCreateRedactEventForEventIds(eventIds: List) { + eventIds.forEach { eventId -> + fakeLocalEchoEventFactory.verifyCreateRedactEvent( + roomId = A_ROOM_ID, + eventId = eventId, + reason = A_REASON + ) + } + } + + private fun verifyCreateLocalEchoForEvents(events: List) { + events.forEach { redactionEvent -> + fakeLocalEchoEventFactory.verifyCreateLocalEcho(redactionEvent) + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt new file mode 100644 index 0000000000..24d9c30039 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/LiveLocationShareRedactionEventProcessorTest.kt @@ -0,0 +1,106 @@ +/* + * 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.location + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe +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.internal.database.model.EventAnnotationsSummaryEntity +import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields +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.EventInsertType +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields +import org.matrix.android.sdk.test.fakes.FakeRealm +import org.matrix.android.sdk.test.fakes.givenDelete +import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenFindFirst + +private const val AN_EVENT_ID = "event-id" +private const val A_REDACTED_EVENT_ID = "redacted-event-id" + +@ExperimentalCoroutinesApi +class LiveLocationShareRedactionEventProcessorTest { + + private val liveLocationShareRedactionEventProcessor = LiveLocationShareRedactionEventProcessor() + private val fakeRealm = FakeRealm() + + @Test + fun `given an event when checking if it should be processed then only event of type REDACTED is processed`() { + val eventId = AN_EVENT_ID + val eventType = EventType.REDACTION + val insertType = EventInsertType.INCREMENTAL_SYNC + + val result = liveLocationShareRedactionEventProcessor.shouldProcess( + eventId = eventId, + eventType = eventType, + insertType = insertType + ) + + result shouldBe true + } + + @Test + fun `given an event when checking if it should be processed then local echo is not processed`() { + val eventId = AN_EVENT_ID + val eventType = EventType.REDACTION + val insertType = EventInsertType.LOCAL_ECHO + + val result = liveLocationShareRedactionEventProcessor.shouldProcess( + eventId = eventId, + eventType = eventType, + insertType = insertType + ) + + result shouldBe false + } + + @Test + fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest { + val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID) + val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first()) + fakeRealm.givenWhere() + .givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(redactedEventEntity) + val liveSummary = mockk() + every { liveSummary.eventId } returns A_REDACTED_EVENT_ID + liveSummary.givenDelete() + fakeRealm.givenWhere() + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(liveSummary) + val annotationsSummary = mockk() + every { annotationsSummary.eventId } returns A_REDACTED_EVENT_ID + annotationsSummary.givenDelete() + fakeRealm.givenWhere() + .givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID) + .givenFindFirst(annotationsSummary) + + liveLocationShareRedactionEventProcessor.process(fakeRealm.instance, event = event) + + verify { + liveSummary.deleteFromRealm() + annotationsSummary.deleteFromRealm() + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt index fbdcf5bfd7..db04b8b8cb 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeEventSenderProcessor.kt @@ -27,4 +27,8 @@ internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() { fun givenPostEventReturns(event: Event, cancelable: Cancelable) { every { postEvent(event) } returns cancelable } + + fun givenPostRedaction(event: Event, reason: String?) { + every { postRedaction(event, reason) } returns mockk() + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt index 50ec85f14a..f484e32149 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeLocalEchoEventFactory.kt @@ -46,24 +46,6 @@ internal class FakeLocalEchoEventFactory { return event } - fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event { - val event = Event() - every { - instance.createLiveLocationEvent( - beaconInfoEventId = any(), - roomId = any(), - latitude = any(), - longitude = any(), - uncertainty = any() - ) - } returns event - - if (withLocalEcho) { - every { instance.createLocalEcho(event) } just runs - } - return event - } - fun verifyCreateStaticLocationEvent( roomId: String, latitude: Double, @@ -82,6 +64,24 @@ internal class FakeLocalEchoEventFactory { } } + fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event { + val event = Event() + every { + instance.createLiveLocationEvent( + beaconInfoEventId = any(), + roomId = any(), + latitude = any(), + longitude = any(), + uncertainty = any() + ) + } returns event + + if (withLocalEcho) { + every { instance.createLocalEcho(event) } just runs + } + return event + } + fun verifyCreateLiveLocationEvent( roomId: String, beaconInfoEventId: String, @@ -100,6 +100,36 @@ internal class FakeLocalEchoEventFactory { } } + fun givenCreateRedactEvent(eventId: String, withLocalEcho: Boolean): Event { + val event = Event() + every { + instance.createRedactEvent( + roomId = any(), + eventId = eventId, + reason = any() + ) + } returns event + + if (withLocalEcho) { + every { instance.createLocalEcho(event) } just runs + } + return event + } + + fun verifyCreateRedactEvent( + roomId: String, + eventId: String, + reason: String? + ) { + verify { + instance.createRedactEvent( + roomId = roomId, + eventId = eventId, + reason = reason + ) + } + } + fun verifyCreateLocalEcho(event: Event) { verify { instance.createLocalEcho(event) } } 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 0ebff87278..cb40889fb7 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 @@ -18,10 +18,13 @@ package org.matrix.android.sdk.test.fakes import io.mockk.MockKVerificationScope import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import io.mockk.verify import io.realm.Realm import io.realm.RealmModel +import io.realm.RealmObject import io.realm.RealmQuery import io.realm.RealmResults import io.realm.kotlin.where @@ -97,3 +100,10 @@ inline fun RealmQuery.givenIsNotNull( every { isNotNull(fieldName) } 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. + */ +fun RealmObject.givenDelete() { + every { deleteFromRealm() } just runs +} 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 new file mode 100644 index 0000000000..15a9823c79 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealmConfiguration.kt @@ -0,0 +1,41 @@ +/* + * 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 + +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.realm.Realm +import io.realm.RealmConfiguration +import org.matrix.android.sdk.internal.database.awaitTransaction + +internal class FakeRealmConfiguration { + + init { + mockkStatic("org.matrix.android.sdk.internal.database.AsyncTransactionKt") + } + + val instance = mockk() + + fun givenAwaitTransaction(realm: Realm) { + val transaction = slot T>() + coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers { + secondArg T>().invoke(realm) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index e305ccbec1..1a68371222 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -48,6 +48,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider +import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever @@ -105,6 +106,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.read.ReadService import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.isLiveLocation import org.matrix.android.sdk.api.session.sync.SyncRequestState import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState import org.matrix.android.sdk.api.session.threads.ThreadNotificationState @@ -135,6 +137,7 @@ class TimelineViewModel @AssistedInject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val locationSharingServiceConnection: LocationSharingServiceConnection, private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, + private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase, timelineFactory: TimelineFactory, appStateHandler: AppStateHandler, ) : VectorViewModel(initialState), @@ -770,7 +773,13 @@ class TimelineViewModel @AssistedInject constructor( private fun handleRedactEvent(action: RoomDetailAction.RedactAction) { val event = room.getTimelineEvent(action.targetEventId) ?: return - room.sendService().redactEvent(event.root, action.reason) + if (event.isLiveLocation()) { + viewModelScope.launch { + redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason) + } + } else { + room.sendService().redactEvent(event.root, action.reason) + } } private fun handleUndoReact(action: RoomDetailAction.UndoReaction) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt new file mode 100644 index 0000000000..ba91000b40 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCase.kt @@ -0,0 +1,30 @@ +/* + * 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.home.room.detail.location + +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.room.Room +import javax.inject.Inject + +class RedactLiveLocationShareEventUseCase @Inject constructor() { + + suspend fun execute(event: Event, room: Room, reason: String?) { + event.eventId + ?.takeUnless { it.isEmpty() } + ?.let { room.locationSharingService().redactLiveLocationShare(it, reason) } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt new file mode 100644 index 0000000000..3bc3a5e351 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCase.kt @@ -0,0 +1,37 @@ +/* + * 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.home.room.detail.timeline.action + +import im.vector.app.core.di.ActiveSessionHolder +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import javax.inject.Inject + +class CheckIfCanRedactEventUseCase @Inject constructor( + private val activeSessionHolder: ActiveSessionHolder +) { + + fun execute(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { + // Only some event types are supported for the moment + val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) + + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + + return event.root.getClearType() in canRedactEventTypes && + // Message sent by the current user can always be redacted, else check permission for messages sent by other users + (event.root.senderId == activeSessionHolder.getActiveSession().myUserId || actionPermissions.canRedact) + } +} 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 30786dc77a..3dfb6744e0 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 @@ -82,6 +82,7 @@ class MessageActionsViewModel @AssistedInject constructor( private val pillsPostProcessorFactory: PillsPostProcessor.Factory, private val vectorPreferences: VectorPreferences, private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase, + private val checkIfCanRedactEventUseCase: CheckIfCanRedactEventUseCase, ) : VectorViewModel(initialState) { private val informationData = initialState.informationData @@ -518,12 +519,7 @@ class MessageActionsViewModel @AssistedInject constructor( } private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { - // Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment - if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false - // Message sent by the current user can always be redacted - if (event.root.senderId == session.myUserId) return true - // Check permission for messages sent by other users - return actionPermissions.canRedact + return checkIfCanRedactEventUseCase.execute(event, actionPermissions) } private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 97ae3b634e..6c5a66d39d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -113,8 +113,14 @@ class TimelineItemFactory @Inject constructor( EventType.CALL_NEGOTIATE, EventType.REACTION, in EventType.POLL_RESPONSE, - in EventType.POLL_END, - in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params) + in EventType.POLL_END -> noticeItemFactory.create(params) + in EventType.BEACON_LOCATION_DATA -> { + if (event.root.isRedacted()) { + messageItemFactory.create(params) + } else { + noticeItemFactory.create(params) + } + } // Calls EventType.CALL_INVITE, EventType.CALL_HANGUP, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index e9f8e35dc9..23db2a721c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -51,7 +51,7 @@ object TimelineDisplayableEvents { EventType.STATE_ROOM_JOIN_RULES, EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_CANCEL, - ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + ) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA } fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt index 8af708fca1..e6765bf35a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -241,6 +241,10 @@ class TimelineEventVisibilityHelper @Inject constructor( } else root.eventId != rootThreadEventId } + if (root.getClearType() in EventType.BEACON_LOCATION_DATA) { + return !root.isRedacted() + } + return false } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt index 69ffc0e89e..fb749c2581 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAndroidService.kt @@ -26,6 +26,7 @@ import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.services.VectorAndroidService import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase import im.vector.app.features.notifications.NotificationUtils +import im.vector.app.features.redaction.CheckIfEventIsRedactedUseCase import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -55,6 +56,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca @Inject lateinit var locationTracker: LocationTracker @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase + @Inject lateinit var checkIfEventIsRedactedUseCase: CheckIfEventIsRedactedUseCase private val binder = LocalBinder() @@ -203,14 +205,18 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) { launchWithActiveSession { session -> val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId) - .distinctUntilChangedBy { it.isActive } - .filter { it.isActive == false } + .distinctUntilChangedBy { it?.isActive } + .filter { it?.isActive == false || (it == null && isLiveRedacted(roomId, beaconEventId)) } .onEach { stopSharingLocation(beaconEventId) } .launchIn(session.coroutineScope) jobs.add(job) } } + private suspend fun isLiveRedacted(roomId: String, beaconEventId: String): Boolean { + return checkIfEventIsRedactedUseCase.execute(roomId = roomId, eventId = beaconEventId) + } + private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) = activeSessionHolder .getSafeActiveSession() diff --git a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt index 0d8b70ccda..bc38889d7f 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt @@ -19,7 +19,7 @@ package im.vector.app.features.location.live import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom @@ -31,13 +31,13 @@ class GetLiveLocationShareSummaryUseCase @Inject constructor( private val session: Session, ) { - suspend fun execute(roomId: String, eventId: String): Flow = withContext(session.coroutineDispatchers.main) { + suspend fun execute(roomId: String, eventId: String): Flow = withContext(session.coroutineDispatchers.main) { Timber.d("getting flow for roomId=$roomId and eventId=$eventId") session.getRoom(roomId) ?.locationSharingService() ?.getLiveLocationShareSummary(eventId) ?.asFlow() - ?.mapNotNull { it.getOrNull() } + ?.map { it.getOrNull() } ?: emptyFlow() } } diff --git a/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.kt b/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.kt new file mode 100644 index 0000000000..ac77455d66 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCase.kt @@ -0,0 +1,39 @@ +/* + * 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.redaction + +import org.matrix.android.sdk.api.session.Session +import timber.log.Timber +import javax.inject.Inject + +class CheckIfEventIsRedactedUseCase @Inject constructor( + private val session: Session, +) { + + suspend fun execute(roomId: String, eventId: String): Boolean { + Timber.d("checking if event is redacted for roomId=$roomId and eventId=$eventId") + return try { + session.eventService() + .getEvent(roomId, eventId) + .isRedacted() + .also { Timber.d("event isRedacted=$it") } + } catch (error: Exception) { + Timber.e(error, "error when getting event, it may not exist yet") + false + } + } +} diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCaseTest.kt new file mode 100644 index 0000000000..2ca285ef50 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/location/RedactLiveLocationShareEventUseCaseTest.kt @@ -0,0 +1,51 @@ +/* + * 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.home.room.detail.location + +import im.vector.app.test.fakes.FakeRoom +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event + +private const val AN_EVENT_ID = "event-id" +private const val A_REASON = "reason" + +class RedactLiveLocationShareEventUseCaseTest { + + private val fakeRoom = FakeRoom() + + private val redactLiveLocationShareEventUseCase = RedactLiveLocationShareEventUseCase() + + @Test + fun `given an event with valid id when calling use case then event is redacted in the room`() = runTest { + val event = Event(eventId = AN_EVENT_ID) + fakeRoom.locationSharingService().givenRedactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON) + + redactLiveLocationShareEventUseCase.execute(event = event, room = fakeRoom, reason = A_REASON) + + fakeRoom.locationSharingService().verifyRedactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON) + } + + @Test + fun `given an event with empty id when calling use case then nothing is done`() = runTest { + val event = Event(eventId = "") + + redactLiveLocationShareEventUseCase.execute(event = event, room = fakeRoom, reason = A_REASON) + + fakeRoom.locationSharingService().verifyRedactLiveLocationShare(inverse = true, beaconInfoEventId = "", reason = A_REASON) + } +} diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt new file mode 100644 index 0000000000..08dd5dac5b --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/action/CheckIfCanRedactEventUseCaseTest.kt @@ -0,0 +1,115 @@ +/* + * 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.home.room.detail.timeline.action + +import im.vector.app.test.fakes.FakeActiveSessionHolder +import io.mockk.mockk +import org.amshove.kluent.shouldBe +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.room.timeline.TimelineEvent + +class CheckIfCanRedactEventUseCaseTest { + + private val fakeActiveSessionHolder = FakeActiveSessionHolder() + + private val checkIfCanRedactEventUseCase = CheckIfCanRedactEventUseCase( + activeSessionHolder = fakeActiveSessionHolder.instance + ) + + @Test + fun `given an event which can be redacted and owned by user when use case executes then the result is true`() { + val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) + + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + + canRedactEventTypes.forEach { eventType -> + val event = givenAnEvent( + eventType = eventType, + senderId = fakeActiveSessionHolder.fakeSession.myUserId + ) + + val actionPermissions = givenActionPermissions(canRedact = false) + + val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions) + + result shouldBe true + } + } + + @Test + fun `given redact permission and an event which can be redacted and sent by another user when use case executes then the result is true`() { + val event = givenAnEvent( + eventType = EventType.MESSAGE, + senderId = "user-id" + ) + + val actionPermissions = givenActionPermissions(canRedact = true) + + val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions) + + result shouldBe true + } + + @Test + fun `given an event which cannot be redacted when use case executes then the result is false`() { + val event = givenAnEvent( + eventType = EventType.CALL_ANSWER, + senderId = fakeActiveSessionHolder.fakeSession.myUserId + ) + + val actionPermissions = givenActionPermissions(canRedact = false) + + val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions) + + result shouldBe false + } + + @Test + fun `given missing redact permission and an event which can be redacted and sent by another user when use case executes then the result is false`() { + val event = givenAnEvent( + eventType = EventType.MESSAGE, + senderId = "user-id" + ) + + val actionPermissions = givenActionPermissions(canRedact = false) + + val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions) + + result shouldBe false + } + + private fun givenAnEvent(eventType: String, senderId: String): TimelineEvent { + val eventId = "event-id" + return TimelineEvent( + root = Event( + eventId = eventId, + type = eventType, + senderId = senderId + ), + localId = 123L, + eventId = eventId, + displayIndex = 1, + ownedByThreadChunk = false, + senderInfo = mockk() + ) + } + + private fun givenActionPermissions(canRedact: Boolean): ActionPermissions { + return ActionPermissions(canRedact = canRedact) + } +} diff --git a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt index fed825154c..ed1bcebf16 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt @@ -53,7 +53,7 @@ class GetLiveLocationShareSummaryUseCaseTest { } @Test - fun `given a room id and event id when calling use case then live data on summary is returned`() = runTest { + fun `given a room id and event id when calling use case then flow on summary is returned`() = runTest { val summary = LiveLocationShareAggregatedSummary( userId = "userId", isActive = true, @@ -70,4 +70,17 @@ class GetLiveLocationShareSummaryUseCaseTest { result shouldBeEqualTo summary } + + @Test + fun `given a room id, event id and a null summary when calling use case then null is emitted in the flow`() = runTest { + fakeSession.roomService() + .getRoom(A_ROOM_ID) + .locationSharingService() + .givenLiveLocationShareSummaryReturns(AN_EVENT_ID, null) + .givenAsFlowReturns(Optional(null)) + + val result = getLiveLocationShareSummaryUseCase.execute(A_ROOM_ID, AN_EVENT_ID).first() + + result shouldBeEqualTo null + } } diff --git a/vector/src/test/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCaseTest.kt new file mode 100644 index 0000000000..7dffd78516 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/redaction/CheckIfEventIsRedactedUseCaseTest.kt @@ -0,0 +1,60 @@ +/* + * 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.redaction + +import im.vector.app.test.fakes.FakeSession +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.UnsignedData + +private const val A_ROOM_ID = "room_id" +private const val AN_EVENT_ID = "event_id" + +class CheckIfEventIsRedactedUseCaseTest { + + private val fakeSession = FakeSession() + + private val checkIfEventIsRedactedUseCase = CheckIfEventIsRedactedUseCase( + session = fakeSession + ) + + @Test + fun `given a room id and event id for redacted event when calling use case then true is returned`() = runTest { + val event = Event( + unsignedData = UnsignedData(age = 123, redactedEvent = Event()) + ) + fakeSession.eventService() + .givenGetEventReturns(event) + + val result = checkIfEventIsRedactedUseCase.execute(A_ROOM_ID, AN_EVENT_ID) + + result shouldBeEqualTo true + } + + @Test + fun `given a room id and event id for non redacted event when calling use case then false is returned`() = runTest { + val event = Event() + fakeSession.eventService() + .givenGetEventReturns(event) + + val result = checkIfEventIsRedactedUseCase.execute(A_ROOM_ID, AN_EVENT_ID) + + result shouldBeEqualTo false + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeEventService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeEventService.kt new file mode 100644 index 0000000000..167f1d624b --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeEventService.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 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.test.fakes + +import io.mockk.coEvery +import io.mockk.mockk +import org.matrix.android.sdk.api.session.events.EventService +import org.matrix.android.sdk.api.session.events.model.Event + +class FakeEventService : EventService by mockk() { + + fun givenGetEventReturns(event: Event) { + coEvery { getEvent(any(), any()) } returns event + } +} diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt index cebd45b2bb..ce498a715a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt @@ -19,8 +19,11 @@ package im.vector.app.test.fakes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary @@ -38,7 +41,7 @@ class FakeLocationSharingService : LocationSharingService by mockk() { fun givenLiveLocationShareSummaryReturns( eventId: String, - summary: LiveLocationShareAggregatedSummary + summary: LiveLocationShareAggregatedSummary? ): LiveData> { return MutableLiveData(Optional(summary)).also { every { getLiveLocationShareSummary(eventId) } returns it @@ -48,4 +51,17 @@ class FakeLocationSharingService : LocationSharingService by mockk() { fun givenStopLiveLocationShareReturns(result: UpdateLiveLocationShareResult) { coEvery { stopLiveLocationShare() } returns result } + + fun givenRedactLiveLocationShare(beaconInfoEventId: String, reason: String?) { + coEvery { redactLiveLocationShare(beaconInfoEventId, reason) } just runs + } + + /** + * @param inverse when true it will check redaction of the live did not happen + * @param beaconInfoEventId event id of the beacon related to the live + * @param reason reason explaining the redaction + */ + fun verifyRedactLiveLocationShare(inverse: Boolean = false, beaconInfoEventId: String, reason: String?) { + coVerify(inverse = inverse) { redactLiveLocationShare(beaconInfoEventId, reason) } + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt index 3af15a7e5c..65295af3dd 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeSession.kt @@ -35,6 +35,7 @@ class FakeSession( val fakeHomeServerCapabilitiesService: FakeHomeServerCapabilitiesService = FakeHomeServerCapabilitiesService(), val fakeSharedSecretStorageService: FakeSharedSecretStorageService = FakeSharedSecretStorageService(), private val fakeRoomService: FakeRoomService = FakeRoomService(), + private val fakeEventService: FakeEventService = FakeEventService(), ) : Session by mockk(relaxed = true) { init { @@ -50,6 +51,7 @@ class FakeSession( override fun homeServerCapabilitiesService(): HomeServerCapabilitiesService = fakeHomeServerCapabilitiesService override fun sharedSecretStorageService() = fakeSharedSecretStorageService override fun roomService() = fakeRoomService + override fun eventService() = fakeEventService fun givenVectorStore(vectorSessionStore: VectorSessionStore) { coEvery {