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()