diff --git a/CHANGES.md b/CHANGES.md index 6f18371100..5bd63edfaa 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,11 @@ Please also refer to the Changelog of Element Android: https://github.com/vector-im/element-android/blob/main/CHANGES.md +Changes in Matrix-SDK v1.5.20 (2023-01-23) +========================================= + +Imported from Element 1.5.20. (https://github.com/vector-im/element-android/releases/tag/v1.5.20) + + Changes in Matrix-SDK v1.5.18 (2023-01-10) ========================================= diff --git a/dependencies.gradle b/dependencies.gradle index b81c1c2017..ee056c1e25 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -11,6 +11,7 @@ def gradle = "7.3.1" def kotlin = "1.7.22" def kotlinCoroutines = "1.6.4" def dagger = "2.44.2" +def firebaseBom = "31.1.1" def appDistribution = "16.0.0-beta05" def retrofit = "2.9.0" def markwon = "4.6.2" @@ -80,10 +81,12 @@ ext.libs = [ ], google : [ 'material' : "com.google.android.material:material:1.7.0", + 'firebaseBom' : "com.google.firebase:firebase-bom:$firebaseBom", + 'messaging' : "com.google.firebase:firebase-messaging", 'appdistributionApi' : "com.google.firebase:firebase-appdistribution-api-ktx:$appDistribution", 'appdistribution' : "com.google.firebase:firebase-appdistribution:$appDistribution", // Phone number https://github.com/google/libphonenumber - 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.3" + 'phonenumber' : "com.googlecode.libphonenumber:libphonenumber:8.13.4" ], dagger : [ 'dagger' : "com.google.dagger:dagger:$dagger", @@ -98,7 +101,7 @@ ext.libs = [ ], element : [ 'opusencoder' : "io.element.android:opusencoder:1.1.0", - 'wysiwyg' : "io.element.android:wysiwyg:0.10.0" + 'wysiwyg' : "io.element.android:wysiwyg:0.14.0" ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", diff --git a/gradle.properties b/gradle.properties index 9905214a33..70c7eb9312 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ vector.httpLogLevel=NONE # Ref: https://github.com/vanniktech/gradle-maven-publish-plugin GROUP=org.matrix.android POM_ARTIFACT_ID=matrix-android-sdk2 -VERSION_NAME=1.5.18 +VERSION_NAME=1.5.20 POM_PACKAGING=aar diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 5b41ddaaec..165dcf079e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -25,6 +25,9 @@ import java.io.IOException import java.net.UnknownHostException import javax.net.ssl.HttpsURLConnection +fun Throwable.is400() = this is Failure.ServerError && + httpCode == HttpsURLConnection.HTTP_BAD_REQUEST + fun Throwable.is401() = this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED && /* 401 */ error.code == MatrixError.M_UNAUTHORIZED diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt new file mode 100644 index 0000000000..071db7f902 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/UserIdentity.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.crypto.crosssigning + +/** + * Container for the three cross signing keys: master, self signing and user signing. + */ +data class UserIdentity( + val masterKey: CryptoCrossSigningKey?, + val selfSigningKey: CryptoCrossSigningKey?, + val userSigningKey: CryptoCrossSigningKey?, +) 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 9b5f4ac19f..9a928c61fb 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 @@ -126,8 +126,37 @@ data class Event( /** * Copy all fields, including transient fields. */ - fun copyAll(): Event { - return copy().also { + + fun copyAll( + type: String? = this.type, + eventId: String? = this.eventId, + content: Content? = this.content, + prevContent: Content? = this.prevContent, + originServerTs: Long? = this.originServerTs, + senderId: String? = this.senderId, + stateKey: String? = this.stateKey, + roomId: String? = this.roomId, + unsignedData: UnsignedData? = this.unsignedData, + redacts: String? = this.redacts, + mxDecryptionResult: OlmDecryptionResult? = this.mxDecryptionResult, + mCryptoError: MXCryptoError.ErrorType? = this.mCryptoError, + mCryptoErrorReason: String? = this.mCryptoErrorReason, + sendState: SendState = this.sendState, + ageLocalTs: Long? = this.ageLocalTs, + threadDetails: ThreadDetails? = this.threadDetails, + ): Event { + return copy( + type = type, + eventId = eventId, + content = content, + prevContent = prevContent, + originServerTs = originServerTs, + senderId = senderId, + stateKey = stateKey, + roomId = roomId, + unsignedData = unsignedData, + redacts = redacts + ).also { it.mxDecryptionResult = mxDecryptionResult it.mCryptoError = mCryptoError it.mCryptoErrorReason = mCryptoErrorReason @@ -429,7 +458,7 @@ fun Event.isReplyRenderedInThread(): Boolean { return isReply() && getRelationContent()?.shouldRenderInThread() == true } -fun Event.isThread(): Boolean = getRelationContentForType(RelationType.THREAD)?.eventId != null +fun Event.isThread(): Boolean = getRootThreadEventId() != null fun Event.getRootThreadEventId(): String? = getRelationContentForType(RelationType.THREAD)?.eventId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt index 5d4d67a65e..e3c5deeee7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/FetchThreadsResult.kt @@ -19,5 +19,4 @@ package org.matrix.android.sdk.api.session.room.threads sealed class FetchThreadsResult { data class ShouldFetchMore(val nextBatch: String) : FetchThreadsResult() object ReachedEnd : FetchThreadsResult() - object Failed : FetchThreadsResult() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt index bb6f6b51d3..dfa6cdeec1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/threads/ThreadsService.kt @@ -23,7 +23,7 @@ import org.matrix.android.sdk.api.session.room.threads.model.ThreadSummary * This interface defines methods to interact with thread related features. * It's the dynamic threads implementation and the homeserver must return * a capability entry for threads. If the server do not support m.thread - * then [ThreadsLocalService] should be used instead + * then [org.matrix.android.sdk.api.session.room.threads.local.ThreadsLocalService] should be used instead */ interface ThreadsService { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt index 468e998407..0a8c58de16 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/login/DefaultLoginWizard.kt @@ -69,7 +69,7 @@ internal class DefaultLoginWizard( ) } else { PasswordLoginParams.userIdentifier( - user = login, + user = login.trim(), password = password, deviceDisplayName = initialDeviceName, deviceId = deviceId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 7862da1c17..50497e3a27 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -89,6 +89,7 @@ import org.matrix.android.sdk.internal.crypto.model.SessionInfo import org.matrix.android.sdk.internal.crypto.model.toRest import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.tasks.DeleteDeviceTask import org.matrix.android.sdk.internal.crypto.tasks.GetDeviceInfoTask import org.matrix.android.sdk.internal.crypto.tasks.GetDevicesTask @@ -192,21 +193,21 @@ internal class DefaultCryptoService @Inject constructor( private val isStarting = AtomicBoolean(false) private val isStarted = AtomicBoolean(false) - fun onStateEvent(roomId: String, event: Event) { + fun onStateEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { when (event.type) { EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) } } - fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean) { + fun onLiveEvent(roomId: String, event: Event, isInitialSync: Boolean, cryptoStoreAggregator: CryptoStoreAggregator?) { // handle state events if (event.isStateEvent()) { when (event.type) { EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) - EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) + EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event, cryptoStoreAggregator) } } @@ -430,8 +431,10 @@ internal class DefaultCryptoService @Inject constructor( * A sync response has been received. * * @param syncResponse the syncResponse + * @param cryptoStoreAggregator data aggregated during the sync response treatment to store */ - fun onSyncCompleted(syncResponse: SyncResponse) { + fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { + cryptoStore.storeData(cryptoStoreAggregator) cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { runCatching { if (syncResponse.deviceLists != null) { @@ -998,15 +1001,26 @@ internal class DefaultCryptoService @Inject constructor( } } - private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { + private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event, cryptoStoreAggregator: CryptoStoreAggregator?) { if (!event.isStateEvent()) return val eventContent = event.content.toModel() val historyVisibility = eventContent?.historyVisibility if (historyVisibility == null) { - cryptoStore.setShouldShareHistory(roomId, false) + if (cryptoStoreAggregator != null) { + cryptoStoreAggregator.setShouldShareHistoryData[roomId] = false + } else { + // Store immediately + cryptoStore.setShouldShareHistory(roomId, false) + } } else { - cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) - cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) + if (cryptoStoreAggregator != null) { + cryptoStoreAggregator.setShouldEncryptForInvitedMembersData[roomId] = historyVisibility != RoomHistoryVisibility.JOINED + cryptoStoreAggregator.setShouldShareHistoryData[roomId] = historyVisibility.shouldShareHistory() + } else { + // Store immediately + cryptoStore.setShouldEncryptForInvitedMembers(roomId, historyVisibility != RoomHistoryVisibility.JOINED) + cryptoStore.setShouldShareHistory(roomId, historyVisibility.shouldShareHistory()) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt index 7e9e156003..364d77f7ac 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DeviceListManager.kt @@ -25,11 +25,13 @@ import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.api.extensions.measureMetric import org.matrix.android.sdk.api.metrics.DownloadDeviceKeysMetricsPlugin import org.matrix.android.sdk.api.session.crypto.crosssigning.DeviceTrustLevel +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.CryptoInfoMapper import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.UserDataToStore import org.matrix.android.sdk.internal.crypto.tasks.DownloadKeysForUsersTask import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.sync.SyncTokenStore @@ -371,6 +373,8 @@ internal class DeviceListManager @Inject constructor( Timber.v("## CRYPTO | doKeyDownloadForUsers() : Got keys for " + filteredUsers.size + " users") } + val userDataToStore = UserDataToStore() + for (userId in filteredUsers) { // al devices = val models = response.deviceKeys?.get(userId)?.mapValues { entry -> CryptoInfoMapper.map(entry.value) } @@ -404,7 +408,7 @@ internal class DeviceListManager @Inject constructor( } // Update the store // Note that devices which aren't in the response will be removed from the stores - cryptoStore.storeUserDevices(userId, workingCopy) + userDataToStore.userDevices[userId] = workingCopy } val masterKey = response.masterKeys?.get(userId)?.toCryptoModel().also { @@ -416,14 +420,15 @@ internal class DeviceListManager @Inject constructor( val userSigningKey = response.userSigningKeys?.get(userId)?.toCryptoModel()?.also { Timber.v("## CRYPTO | CrossSigning : Got keys for $userId : USK ${it.unpaddedBase64PublicKey}") } - cryptoStore.storeUserCrossSigningKeys( - userId, - masterKey, - selfSigningKey, - userSigningKey + userDataToStore.userIdentities[userId] = UserIdentity( + masterKey = masterKey, + selfSigningKey = selfSigningKey, + userSigningKey = userSigningKey ) } + cryptoStore.storeData(userDataToStore) + // Update devices trust for these users // dispatchDeviceChange(downloadUsers) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt index 21e3342365..0305f73a7b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/IMXCryptoStore.kt @@ -22,9 +22,9 @@ import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.store.db.model.KeysBackupDataEntity import org.matrix.olm.OlmAccount import org.matrix.olm.OlmOutboundGroupSession @@ -230,11 +231,12 @@ internal interface IMXCryptoStore { */ fun storeUserDevices(userId: String, devices: Map?) - fun storeUserCrossSigningKeys( + /** + * Store the cross signing keys for the user userId. + */ + fun storeUserIdentity( userId: String, - masterKey: CryptoCrossSigningKey?, - selfSigningKey: CryptoCrossSigningKey?, - userSigningKey: CryptoCrossSigningKey? + userIdentity: UserIdentity ) /** @@ -290,6 +292,13 @@ internal interface IMXCryptoStore { fun shouldEncryptForInvitedMembers(roomId: String): Boolean + /** + * Sets a boolean flag that will determine whether or not this device should encrypt Events for + * invited members. + * + * @param roomId the room id + * @param shouldEncryptForInvitedMembers The boolean flag + */ fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) fun shouldShareHistory(roomId: String): Boolean @@ -580,4 +589,14 @@ internal interface IMXCryptoStore { fun areDeviceKeysUploaded(): Boolean fun tidyUpDataBase() fun getOutgoingRoomKeyRequests(inStates: Set): List + + /** + * Store a bunch of data collected during a sync response treatment. @See [CryptoStoreAggregator]. + */ + fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) + + /** + * Store a bunch of data related to the users. @See [UserDataToStore]. + */ + fun storeData(userDataToStore: UserDataToStore) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt new file mode 100644 index 0000000000..914ce4704e --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/UserDataToStore.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store + +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity +import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo + +internal data class UserDataToStore( + /** + * Map of userId -> (Map of deviceId -> [CryptoDeviceInfo]). + */ + val userDevices: MutableMap> = mutableMapOf(), + /** + * Map of userId -> [UserIdentity]. + */ + val userIdentities: MutableMap = mutableMapOf(), +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt new file mode 100644 index 0000000000..687ec95ec3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/CryptoStoreAggregator.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.store.db + +data class CryptoStoreAggregator( + val setShouldShareHistoryData: MutableMap = mutableMapOf(), + val setShouldEncryptForInvitedMembersData: MutableMap = mutableMapOf(), +) { + fun isEmpty(): Boolean { + return setShouldShareHistoryData.isEmpty() && + setShouldEncryptForInvitedMembersData.isEmpty() + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt index 2d66ce1488..6412df205f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/Helper.kt @@ -20,10 +20,12 @@ import android.util.Base64 import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmObject +import timber.log.Timber import java.io.ByteArrayOutputStream import java.io.ObjectOutputStream import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream +import kotlin.system.measureTimeMillis /** * Get realm, invoke the action, close realm, and return the result of the action. @@ -55,10 +57,12 @@ internal fun doRealmQueryAndCopyList(realmConfiguration: Realm /** * Get realm instance, invoke the action in a transaction and close realm. */ -internal fun doRealmTransaction(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { - Realm.getInstance(realmConfiguration).use { realm -> - realm.executeTransaction { action.invoke(it) } - } +internal fun doRealmTransaction(tag: String, realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { + measureTimeMillis { + Realm.getInstance(realmConfiguration).use { realm -> + realm.executeTransaction { action.invoke(it) } + } + }.also { Timber.w("doRealmTransaction for $tag took $it millis") } } internal fun doRealmTransactionAsync(realmConfiguration: RealmConfiguration, action: (Realm) -> Unit) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt index 1b52b79746..b4368467a2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/store/db/RealmCryptoStore.kt @@ -33,9 +33,9 @@ import org.matrix.android.sdk.api.session.crypto.GlobalCryptoConfig import org.matrix.android.sdk.api.session.crypto.NewSessionListener import org.matrix.android.sdk.api.session.crypto.OutgoingKeyRequest import org.matrix.android.sdk.api.session.crypto.OutgoingRoomKeyRequestState -import org.matrix.android.sdk.api.session.crypto.crosssigning.CryptoCrossSigningKey import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.PrivateKeysInfo +import org.matrix.android.sdk.api.session.crypto.crosssigning.UserIdentity import org.matrix.android.sdk.api.session.crypto.keysbackup.SavedKeyBackupKeyInfo import org.matrix.android.sdk.api.session.crypto.model.AuditTrail import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo @@ -55,6 +55,7 @@ import org.matrix.android.sdk.internal.crypto.model.MXInboundMegolmSessionWrappe import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OutboundGroupSessionWrapper import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore +import org.matrix.android.sdk.internal.crypto.store.UserDataToStore import org.matrix.android.sdk.internal.crypto.store.db.mapper.CrossSigningKeysMapper import org.matrix.android.sdk.internal.crypto.store.db.mapper.MyDeviceLastSeenInfoEntityMapper import org.matrix.android.sdk.internal.crypto.store.db.model.AuditTrailEntity @@ -147,7 +148,7 @@ internal class RealmCryptoStore @Inject constructor( init { // Ensure CryptoMetadataEntity is inserted in DB - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("init", realmConfiguration) { realm -> var currentMetadata = realm.where().findFirst() var deleteAll = false @@ -189,7 +190,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun deleteStore() { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("deleteStore", realmConfiguration) { it.deleteAll() } } @@ -218,7 +219,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun storeDeviceId(deviceId: String) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("storeDeviceId", realmConfiguration) { it.where().findFirst()?.deviceId = deviceId } } @@ -230,7 +231,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun saveOlmAccount() { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("saveOlmAccount", realmConfiguration) { it.where().findFirst()?.putOlmAccount(olmAccount) } } @@ -248,7 +249,7 @@ internal class RealmCryptoStore @Inject constructor( @Synchronized override fun getOrCreateOlmAccount(): OlmAccount { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("getOrCreateOlmAccount", realmConfiguration) { val metaData = it.where().findFirst() val existing = metaData!!.getOlmAccount() if (existing == null) { @@ -288,129 +289,139 @@ internal class RealmCryptoStore @Inject constructor( } override fun storeUserDevices(userId: String, devices: Map?) { - doRealmTransaction(realmConfiguration) { realm -> - if (devices == null) { - Timber.d("Remove user $userId") - // Remove the user - UserEntity.delete(realm, userId) - } else { - val userEntity = UserEntity.getOrCreate(realm, userId) - // First delete the removed devices - val deviceIds = devices.keys - userEntity.devices.toTypedArray().iterator().let { - while (it.hasNext()) { - val deviceInfoEntity = it.next() - if (deviceInfoEntity.deviceId !in deviceIds) { - Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId") - deviceInfoEntity.deleteOnCascade() - } + doRealmTransaction("storeUserDevices", realmConfiguration) { realm -> + storeUserDevices(realm, userId, devices) + } + } + + private fun storeUserDevices(realm: Realm, userId: String, devices: Map?) { + if (devices == null) { + Timber.d("Remove user $userId") + // Remove the user + UserEntity.delete(realm, userId) + } else { + val userEntity = UserEntity.getOrCreate(realm, userId) + // First delete the removed devices + val deviceIds = devices.keys + userEntity.devices.toTypedArray().iterator().let { + while (it.hasNext()) { + val deviceInfoEntity = it.next() + if (deviceInfoEntity.deviceId !in deviceIds) { + Timber.d("Remove device ${deviceInfoEntity.deviceId} of user $userId") + deviceInfoEntity.deleteOnCascade() } } - // Then update existing devices or add new one - devices.values.forEach { cryptoDeviceInfo -> - val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } - if (existingDeviceInfoEntity == null) { - // Add the device - Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") - val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) - newEntity.firstTimeSeenLocalTs = clock.epochMillis() - userEntity.devices.add(newEntity) - } else { - // Update the device - Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") - CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) - } + } + // Then update existing devices or add new one + devices.values.forEach { cryptoDeviceInfo -> + val existingDeviceInfoEntity = userEntity.devices.firstOrNull { it.deviceId == cryptoDeviceInfo.deviceId } + if (existingDeviceInfoEntity == null) { + // Add the device + Timber.d("Add device ${cryptoDeviceInfo.deviceId} of user $userId") + val newEntity = CryptoMapper.mapToEntity(cryptoDeviceInfo) + newEntity.firstTimeSeenLocalTs = clock.epochMillis() + userEntity.devices.add(newEntity) + } else { + // Update the device + Timber.d("Update device ${cryptoDeviceInfo.deviceId} of user $userId") + CryptoMapper.updateDeviceInfoEntity(existingDeviceInfoEntity, cryptoDeviceInfo) } } } } - override fun storeUserCrossSigningKeys( + override fun storeUserIdentity( userId: String, - masterKey: CryptoCrossSigningKey?, - selfSigningKey: CryptoCrossSigningKey?, - userSigningKey: CryptoCrossSigningKey? + userIdentity: UserIdentity, ) { - doRealmTransaction(realmConfiguration) { realm -> - UserEntity.getOrCreate(realm, userId) - .let { userEntity -> - if (masterKey == null || selfSigningKey == null) { - // The user has disabled cross signing? - userEntity.crossSigningInfoEntity?.deleteOnCascade() - userEntity.crossSigningInfoEntity = null - } else { - var shouldResetMyDevicesLocalTrust = false - CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> - // What should we do if we detect a change of the keys? - val existingMaster = signingInfo.getMasterKey() - if (existingMaster != null && existingMaster.publicKeyBase64 == masterKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingMaster, masterKey) - } else { - Timber.d("## CrossSigning MSK change for $userId") - val keyEntity = crossSigningKeysMapper.map(masterKey) - signingInfo.setMasterKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my msk has changed! clear my private key - // Could we have some race here? e.g I am the one that did change the keys - // could i get this update to early and clear the private keys? - // -> initializeCrossSigning is guarding for that by storing all at once - realm.where().findFirst()?.apply { - xSignMasterPrivateKey = null - } + doRealmTransaction("storeUserIdentity", realmConfiguration) { realm -> + storeUserIdentity(realm, userId, userIdentity) + } + } + + private fun storeUserIdentity( + realm: Realm, + userId: String, + userIdentity: UserIdentity, + ) { + UserEntity.getOrCreate(realm, userId) + .let { userEntity -> + if (userIdentity.masterKey == null || userIdentity.selfSigningKey == null) { + // The user has disabled cross signing? + userEntity.crossSigningInfoEntity?.deleteOnCascade() + userEntity.crossSigningInfoEntity = null + } else { + var shouldResetMyDevicesLocalTrust = false + CrossSigningInfoEntity.getOrCreate(realm, userId).let { signingInfo -> + // What should we do if we detect a change of the keys? + val existingMaster = signingInfo.getMasterKey() + if (existingMaster != null && existingMaster.publicKeyBase64 == userIdentity.masterKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingMaster, userIdentity.masterKey) + } else { + Timber.d("## CrossSigning MSK change for $userId") + val keyEntity = crossSigningKeysMapper.map(userIdentity.masterKey) + signingInfo.setMasterKey(keyEntity) + if (userId == this.userId) { + shouldResetMyDevicesLocalTrust = true + // my msk has changed! clear my private key + // Could we have some race here? e.g I am the one that did change the keys + // could i get this update to early and clear the private keys? + // -> initializeCrossSigning is guarding for that by storing all at once + realm.where().findFirst()?.apply { + xSignMasterPrivateKey = null + } + } + } + + val existingSelfSigned = signingInfo.getSelfSignedKey() + if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == userIdentity.selfSigningKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingSelfSigned, userIdentity.selfSigningKey) + } else { + Timber.d("## CrossSigning SSK change for $userId") + val keyEntity = crossSigningKeysMapper.map(userIdentity.selfSigningKey) + signingInfo.setSelfSignedKey(keyEntity) + if (userId == this.userId) { + shouldResetMyDevicesLocalTrust = true + // my ssk has changed! clear my private key + realm.where().findFirst()?.apply { + xSignSelfSignedPrivateKey = null } } + } - val existingSelfSigned = signingInfo.getSelfSignedKey() - if (existingSelfSigned != null && existingSelfSigned.publicKeyBase64 == selfSigningKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingSelfSigned, selfSigningKey) + // Only for me + if (userIdentity.userSigningKey != null) { + val existingUSK = signingInfo.getUserSigningKey() + if (existingUSK != null && existingUSK.publicKeyBase64 == userIdentity.userSigningKey.unpaddedBase64PublicKey) { + crossSigningKeysMapper.update(existingUSK, userIdentity.userSigningKey) } else { - Timber.d("## CrossSigning SSK change for $userId") - val keyEntity = crossSigningKeysMapper.map(selfSigningKey) - signingInfo.setSelfSignedKey(keyEntity) + Timber.d("## CrossSigning USK change for $userId") + val keyEntity = crossSigningKeysMapper.map(userIdentity.userSigningKey) + signingInfo.setUserSignedKey(keyEntity) if (userId == this.userId) { shouldResetMyDevicesLocalTrust = true - // my ssk has changed! clear my private key + // my usk has changed! clear my private key realm.where().findFirst()?.apply { - xSignSelfSignedPrivateKey = null + xSignUserPrivateKey = null } } } + } - // Only for me - if (userSigningKey != null) { - val existingUSK = signingInfo.getUserSigningKey() - if (existingUSK != null && existingUSK.publicKeyBase64 == userSigningKey.unpaddedBase64PublicKey) { - crossSigningKeysMapper.update(existingUSK, userSigningKey) - } else { - Timber.d("## CrossSigning USK change for $userId") - val keyEntity = crossSigningKeysMapper.map(userSigningKey) - signingInfo.setUserSignedKey(keyEntity) - if (userId == this.userId) { - shouldResetMyDevicesLocalTrust = true - // my usk has changed! clear my private key - realm.where().findFirst()?.apply { - xSignUserPrivateKey = null - } + // When my cross signing keys are reset, we consider clearing all existing device trust + if (shouldResetMyDevicesLocalTrust) { + realm.where() + .equalTo(UserEntityFields.USER_ID, this.userId) + .findFirst() + ?.devices?.forEach { + it?.trustLevelEntity?.crossSignedVerified = false + it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId } - } - } - - // When my cross signing keys are reset, we consider clearing all existing device trust - if (shouldResetMyDevicesLocalTrust) { - realm.where() - .equalTo(UserEntityFields.USER_ID, this.userId) - .findFirst() - ?.devices?.forEach { - it?.trustLevelEntity?.crossSignedVerified = false - it?.trustLevelEntity?.locallyVerified = it.deviceId == deviceId - } - } - userEntity.crossSigningInfoEntity = signingInfo } + userEntity.crossSigningInfoEntity = signingInfo } } - } + } } override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { @@ -480,7 +491,7 @@ internal class RealmCryptoStore @Inject constructor( override fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?) { Timber.v("## CRYPTO | *** storePrivateKeysInfo ${msk != null}, ${usk != null}, ${ssk != null}") - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storePrivateKeysInfo", realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignMasterPrivateKey = msk xSignUserPrivateKey = usk @@ -490,7 +501,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("saveBackupRecoveryKey", realmConfiguration) { realm -> realm.where().findFirst()?.apply { keyBackupRecoveryKey = recoveryKey keyBackupRecoveryKeyVersion = version @@ -516,7 +527,7 @@ internal class RealmCryptoStore @Inject constructor( override fun storeMSKPrivateKey(msk: String?) { Timber.v("## CRYPTO | *** storeMSKPrivateKey ${msk != null} ") - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storeMSKPrivateKey", realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignMasterPrivateKey = msk } @@ -525,7 +536,7 @@ internal class RealmCryptoStore @Inject constructor( override fun storeSSKPrivateKey(ssk: String?) { Timber.v("## CRYPTO | *** storeSSKPrivateKey ${ssk != null} ") - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storeSSKPrivateKey", realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignSelfSignedPrivateKey = ssk } @@ -534,7 +545,7 @@ internal class RealmCryptoStore @Inject constructor( override fun storeUSKPrivateKey(usk: String?) { Timber.v("## CRYPTO | *** storeUSKPrivateKey ${usk != null} ") - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storeUSKPrivateKey", realmConfiguration) { realm -> realm.where().findFirst()?.apply { xSignUserPrivateKey = usk } @@ -667,7 +678,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun storeRoomAlgorithm(roomId: String, algorithm: String?) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("storeRoomAlgorithm", realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).let { entity -> entity.algorithm = algorithm // store anyway the new algorithm, but mark the room @@ -708,7 +719,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setShouldEncryptForInvitedMembers(roomId: String, shouldEncryptForInvitedMembers: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setShouldEncryptForInvitedMembers", realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).shouldEncryptForInvitedMembers = shouldEncryptForInvitedMembers } } @@ -716,7 +727,7 @@ internal class RealmCryptoStore @Inject constructor( override fun setShouldShareHistory(roomId: String, shouldShareHistory: Boolean) { Timber.tag(loggerTag.value) .v("setShouldShareHistory for room $roomId is $shouldShareHistory") - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setShouldShareHistory", realmConfiguration) { CryptoRoomEntity.getOrCreate(it, roomId).shouldShareHistory = shouldShareHistory } } @@ -733,7 +744,7 @@ internal class RealmCryptoStore @Inject constructor( if (sessionIdentifier != null) { val key = OlmSessionEntity.createPrimaryKey(sessionIdentifier, deviceKey) - doRealmTransaction(realmConfiguration) { + doRealmTransaction("storeSession", realmConfiguration) { val realmOlmSession = OlmSessionEntity().apply { primaryKey = key sessionId = sessionIdentifier @@ -790,7 +801,7 @@ internal class RealmCryptoStore @Inject constructor( return } - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("storeInboundGroupSessions", realmConfiguration) { realm -> sessions.forEach { wrapper -> val sessionIdentifier = try { @@ -914,7 +925,7 @@ internal class RealmCryptoStore @Inject constructor( override fun removeInboundGroupSession(sessionId: String, senderKey: String) { val key = OlmInboundGroupSessionEntity.createPrimaryKey(sessionId, senderKey) - doRealmTransaction(realmConfiguration) { + doRealmTransaction("removeInboundGroupSession", realmConfiguration) { it.where() .equalTo(OlmInboundGroupSessionEntityFields.PRIMARY_KEY, key) .findAll() @@ -933,7 +944,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setKeyBackupVersion(keyBackupVersion: String?) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setKeyBackupVersion", realmConfiguration) { it.where().findFirst()?.backupVersion = keyBackupVersion } } @@ -945,7 +956,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setKeysBackupData(keysBackupData: KeysBackupDataEntity?) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setKeysBackupData", realmConfiguration) { if (keysBackupData == null) { // Clear the table it.where() @@ -959,7 +970,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun resetBackupMarkers() { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("resetBackupMarkers", realmConfiguration) { it.where() .findAll() .map { inboundGroupSession -> @@ -973,7 +984,7 @@ internal class RealmCryptoStore @Inject constructor( return } - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("markBackupDoneForInboundGroupSessions", realmConfiguration) { realm -> olmInboundGroupSessionWrappers.forEach { olmInboundGroupSessionWrapper -> try { val sessionIdentifier = @@ -1032,13 +1043,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun setGlobalBlacklistUnverifiedDevices(block: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setGlobalBlacklistUnverifiedDevices", realmConfiguration) { it.where().findFirst()?.globalBlacklistUnverifiedDevices = block } } override fun enableKeyGossiping(enable: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("enableKeyGossiping", realmConfiguration) { it.where().findFirst()?.globalEnableKeyGossiping = enable } } @@ -1062,13 +1073,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun enableShareKeyOnInvite(enable: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("enableShareKeyOnInvite", realmConfiguration) { it.where().findFirst()?.enableKeyForwardingOnInvite = enable } } override fun setDeviceKeysUploaded(uploaded: Boolean) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("setDeviceKeysUploaded", realmConfiguration) { it.where().findFirst()?.deviceKeysSentToServer = uploaded } } @@ -1115,7 +1126,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun blockUnverifiedDevicesInRoom(roomId: String, block: Boolean) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("blockUnverifiedDevicesInRoom", realmConfiguration) { realm -> CryptoRoomEntity.getById(realm, roomId) ?.blacklistUnverifiedDevices = block } @@ -1135,7 +1146,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun saveDeviceTrackingStatuses(deviceTrackingStatuses: Map) { - doRealmTransaction(realmConfiguration) { + doRealmTransaction("saveDeviceTrackingStatuses", realmConfiguration) { deviceTrackingStatuses .map { entry -> UserEntity.getOrCreate(it, entry.key) @@ -1268,7 +1279,7 @@ internal class RealmCryptoStore @Inject constructor( ): OutgoingKeyRequest { // Insert the request and return the one passed in parameter lateinit var request: OutgoingKeyRequest - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("getOrAddOutgoingRoomKeyRequest", realmConfiguration) { realm -> val existing = realm.where() .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, requestBody.sessionId) @@ -1306,7 +1317,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun updateOutgoingRoomKeyRequestState(requestId: String, newState: OutgoingRoomKeyRequestState) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("updateOutgoingRoomKeyRequestState", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .findFirst()?.apply { @@ -1320,7 +1331,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun updateOutgoingRoomKeyRequiredIndex(requestId: String, newIndex: Int) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("updateOutgoingRoomKeyRequiredIndex", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .findFirst()?.apply { @@ -1337,7 +1348,7 @@ internal class RealmCryptoStore @Inject constructor( fromDevice: String?, event: Event ) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("updateOutgoingRoomKeyReply", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.ROOM_ID, roomId) .equalTo(OutgoingKeyRequestEntityFields.MEGOLM_SESSION_ID, sessionId) @@ -1353,7 +1364,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun deleteOutgoingRoomKeyRequest(requestId: String) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("deleteOutgoingRoomKeyRequest", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_ID, requestId) .findFirst()?.deleteOnCascade() @@ -1361,7 +1372,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun deleteOutgoingRoomKeyRequestInState(state: OutgoingRoomKeyRequestState) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("deleteOutgoingRoomKeyRequestInState", realmConfiguration) { realm -> realm.where() .equalTo(OutgoingKeyRequestEntityFields.REQUEST_STATE_STR, state.name) .findAll() @@ -1497,7 +1508,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setMyCrossSigningInfo(info: MXCrossSigningInfo?) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("setMyCrossSigningInfo", realmConfiguration) { realm -> realm.where().findFirst()?.userId?.let { userId -> addOrUpdateCrossSigningInfo(realm, userId, info) } @@ -1505,7 +1516,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setUserKeysAsTrusted(userId: String, trusted: Boolean) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("setUserKeysAsTrusted", realmConfiguration) { realm -> val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .findFirst() @@ -1525,7 +1536,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun setDeviceTrust(userId: String, deviceId: String, crossSignedVerified: Boolean, locallyVerified: Boolean?) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("setDeviceTrust", realmConfiguration) { realm -> realm.where(DeviceInfoEntity::class.java) .equalTo(DeviceInfoEntityFields.PRIMARY_KEY, DeviceInfoEntity.createPrimaryKey(userId, deviceId)) .findFirst()?.let { deviceInfoEntity -> @@ -1545,7 +1556,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun clearOtherUserTrust() { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("clearOtherUserTrust", realmConfiguration) { realm -> val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) .findAll() xInfoEntities?.forEach { info -> @@ -1560,7 +1571,7 @@ internal class RealmCryptoStore @Inject constructor( } override fun updateUsersTrust(check: (String) -> Boolean) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("updateUsersTrust", realmConfiguration) { realm -> val xInfoEntities = realm.where(CrossSigningInfoEntity::class.java) .findAll() xInfoEntities?.forEach { xInfoEntity -> @@ -1668,13 +1679,13 @@ internal class RealmCryptoStore @Inject constructor( } override fun setCrossSigningInfo(userId: String, info: MXCrossSigningInfo?) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("setCrossSigningInfo", realmConfiguration) { realm -> addOrUpdateCrossSigningInfo(realm, userId, info) } } override fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("markMyMasterKeyAsLocallyTrusted", realmConfiguration) { realm -> realm.where().findFirst()?.userId?.let { myUserId -> CrossSigningInfoEntity.get(realm, myUserId)?.getMasterKey()?.let { xInfoEntity -> val level = xInfoEntity.trustLevelEntity @@ -1713,7 +1724,7 @@ internal class RealmCryptoStore @Inject constructor( val roomId = withHeldContent.roomId ?: return val sessionId = withHeldContent.sessionId ?: return if (withHeldContent.algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("addWithHeldMegolmSession", realmConfiguration) { realm -> WithHeldSessionEntity.getOrCreate(realm, roomId, sessionId)?.let { it.code = withHeldContent.code it.senderKey = withHeldContent.senderKey @@ -1745,7 +1756,7 @@ internal class RealmCryptoStore @Inject constructor( deviceIdentityKey: String, chainIndex: Int ) { - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("markedSessionAsShared", realmConfiguration) { realm -> SharedSessionEntity.create( realm = realm, roomId = roomId, @@ -1794,7 +1805,7 @@ internal class RealmCryptoStore @Inject constructor( */ override fun tidyUpDataBase() { val prevWeekTs = clock.epochMillis() - 7 * 24 * 60 * 60 * 1_000 - doRealmTransaction(realmConfiguration) { realm -> + doRealmTransaction("tidyUpDataBase", realmConfiguration) { realm -> // Clean the old ones? realm.where() @@ -1815,4 +1826,31 @@ internal class RealmCryptoStore @Inject constructor( // Can we do something for WithHeldSessionEntity? } } + + override fun storeData(cryptoStoreAggregator: CryptoStoreAggregator) { + if (cryptoStoreAggregator.isEmpty()) { + return + } + doRealmTransaction("storeData - CryptoStoreAggregator", realmConfiguration) { realm -> + // setShouldShareHistory + cryptoStoreAggregator.setShouldShareHistoryData.forEach { + CryptoRoomEntity.getOrCreate(realm, it.key).shouldShareHistory = it.value + } + // setShouldEncryptForInvitedMembers + cryptoStoreAggregator.setShouldEncryptForInvitedMembersData.forEach { + CryptoRoomEntity.getOrCreate(realm, it.key).shouldEncryptForInvitedMembers = it.value + } + } + } + + override fun storeData(userDataToStore: UserDataToStore) { + doRealmTransaction("storeData - UserDataToStore", realmConfiguration) { realm -> + userDataToStore.userDevices.forEach { + storeUserDevices(realm, it.key, it.value) + } + userDataToStore.userIdentities.forEach { + storeUserIdentity(realm, it.key, it.value) + } + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt index 9bd197e42e..f89221b627 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/di/SerializeNulls.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.di -import androidx.annotation.Nullable import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonQualifier import com.squareup.moshi.Moshi @@ -28,7 +27,6 @@ import java.lang.reflect.Type internal annotation class SerializeNulls { companion object { val JSON_ADAPTER_FACTORY: JsonAdapter.Factory = object : JsonAdapter.Factory { - @Nullable override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { val nextAnnotations = Types.nextAnnotations(annotations, SerializeNulls::class.java) ?: return null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt index 4e0525536c..334a8c5076 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/interceptors/FormattedJsonHttpLogger.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.network.interceptors -import androidx.annotation.NonNull import okhttp3.logging.HttpLoggingInterceptor import org.json.JSONArray import org.json.JSONException @@ -38,7 +37,7 @@ internal class FormattedJsonHttpLogger( * @param message */ @Synchronized - override fun log(@NonNull message: String) { + override fun log(message: String) { Timber.v(message) // Try to log formatted Json only if there is a chance that [message] contains Json. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt index 8b54978279..6c28b9fcce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/network/parsing/CheckNumberType.kt @@ -16,7 +16,6 @@ package org.matrix.android.sdk.internal.network.parsing -import androidx.annotation.Nullable import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter @@ -32,14 +31,12 @@ internal interface CheckNumberType { companion object { val JSON_ADAPTER_FACTORY = object : JsonAdapter.Factory { - @Nullable override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (type !== Any::class.java) { return null } val delegate: JsonAdapter = moshi.nextAdapter(this, Any::class.java, emptySet()) return object : JsonAdapter() { - @Nullable @Throws(IOException::class) override fun fromJson(reader: JsonReader): Any? { return if (reader.peek() !== JsonReader.Token.NUMBER) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt index cfc26045a0..ce34b0430e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/StreamEventsManager.kt @@ -42,14 +42,12 @@ internal class StreamEventsManager @Inject constructor() { listeners.remove(listener) } - fun dispatchLiveEventReceived(event: Event, roomId: String, initialSync: Boolean) { + fun dispatchLiveEventReceived(event: Event, roomId: String) { Timber.v("## dispatchLiveEventReceived ${event.eventId}") coroutineScope.launch { - if (!initialSync) { - listeners.forEach { - tryOrNull { - it.onLiveEvent(roomId, event) - } + listeners.forEach { + tryOrNull { + it.onLiveEvent(roomId, event) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt index 5f35c919fc..e359410f17 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/GetPushRulesResponse.kt @@ -30,10 +30,4 @@ internal data class GetPushRulesResponse( */ @Json(name = "global") val global: RuleSet, - - /** - * Device specific rules, apply only to current device. - */ - @Json(name = "device") - val device: RuleSet? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt index 88c78aa460..4a46f56a70 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/pushers/SavePushRulesTask.kt @@ -42,7 +42,6 @@ internal class DefaultSavePushRulesTask @Inject constructor(@SessionDatabase pri .findAll() .forEach { it.deleteOnCascade() } - // Save only global rules for the moment val globalRules = params.pushRules.global val content = PushRulesEntity(RuleScope.GLOBAL).apply { kind = RuleSetKey.CONTENT } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt index 793c2573be..653069b3c8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateLocalRoomTask.kt @@ -176,7 +176,7 @@ internal class DefaultCreateLocalRoomTask @Inject constructor( } // Give info to crypto module - cryptoService.onStateEvent(roomId, event) + cryptoService.onStateEvent(roomId, event, null) } roomMemberContentsByUser.getOrPut(event.senderId) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index 8be6b26249..d974c597ac 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -597,26 +597,22 @@ internal class LocalEchoEventFactory @Inject constructor( return clock.epochMillis() } - /** - * Creates a reply to a regular timeline Event or a thread Event if needed. - */ - fun createReplyTextEvent( - roomId: String, + fun createReplyTextContent( eventReplied: TimelineEvent, replyText: CharSequence, replyTextFormatted: CharSequence?, autoMarkdown: Boolean, rootThreadEventId: String? = null, showInThread: Boolean, - additionalContent: Content? = null - ): Event? { + isRedactedEvent: Boolean = false + ): MessageContent? { // Fallbacks and event representation // TODO Add error/warning logs when any of this is null val permalink = permalinkFactory.createPermalink(eventReplied.root, false) ?: return null val userId = eventReplied.root.senderId ?: return null val userLink = permalinkFactory.createPermalink(userId, false) ?: return null - val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply()) + val body = bodyForReply(eventReplied.getLastMessageContent(), eventReplied.isReply(), isRedactedEvent) // As we always supply formatted body for replies we should force the MarkdownParser to produce html. val finalReplyTextFormatted = replyTextFormatted?.toString() ?: markdownParser.parse(replyText, force = true, advanced = autoMarkdown).takeFormatted() @@ -635,7 +631,7 @@ internal class LocalEchoEventFactory @Inject constructor( val replyFallback = buildReplyFallback(body, userId, replyText.toString()) val eventId = eventReplied.root.eventId ?: return null - val content = MessageTextContent( + return MessageTextContent( msgType = MessageType.MSGTYPE_TEXT, format = MessageFormat.FORMAT_MATRIX_HTML, body = replyFallback, @@ -646,7 +642,25 @@ internal class LocalEchoEventFactory @Inject constructor( showInThread = showInThread ) ) - return createMessageEvent(roomId, content, additionalContent) + } + + /** + * Creates a reply to a regular timeline Event or a thread Event if needed. + */ + fun createReplyTextEvent( + roomId: String, + eventReplied: TimelineEvent, + replyText: CharSequence, + replyTextFormatted: CharSequence?, + autoMarkdown: Boolean, + rootThreadEventId: String? = null, + showInThread: Boolean, + additionalContent: Content? = null, + ): Event? { + val content = createReplyTextContent(eventReplied, replyText, replyTextFormatted, autoMarkdown, rootThreadEventId, showInThread) + return content?.let { + createMessageEvent(roomId, it, additionalContent) + } } private fun generateThreadRelationContent(rootThreadEventId: String) = @@ -715,7 +729,7 @@ internal class LocalEchoEventFactory @Inject constructor( * In case of an edit of a reply the last content is not * himself a reply, but it will contain the fallbacks, so we have to trim them. */ - private fun bodyForReply(content: MessageContent?, isReply: Boolean): TextContent { + fun bodyForReply(content: MessageContent?, isReply: Boolean, isRedactedEvent: Boolean = false): TextContent { when (content?.msgType) { MessageType.MSGTYPE_EMOTE, MessageType.MSGTYPE_TEXT, @@ -724,7 +738,9 @@ internal class LocalEchoEventFactory @Inject constructor( if (content is MessageContentWithFormattedBody) { formattedText = content.matrixFormattedBody } - return if (isReply) { + return if (isRedactedEvent) { + TextContent("message removed.") + } else if (isReply) { TextContent(content.body, formattedText).removeInReplyFallbacks() } else { TextContent(content.body, formattedText) @@ -738,7 +754,11 @@ internal class LocalEchoEventFactory @Inject constructor( MessageType.MSGTYPE_POLL_START -> { return TextContent((content as? MessagePollContent)?.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "") } - else -> return TextContent(content?.body ?: "") + else -> { + return if (isRedactedEvent) { + TextContent("message removed.") + } else TextContent(content?.body ?: "") + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 0854cc5cf4..3ce8ea658d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -42,6 +42,7 @@ import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler @@ -63,6 +64,7 @@ internal class DefaultTimeline( private val settings: TimelineSettings, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val clock: Clock, + localEchoEventFactory: LocalEchoEventFactory, stateEventDataSource: StateEventDataSource, paginationTask: PaginationTask, getEventTask: GetContextOfEventTask, @@ -114,6 +116,7 @@ internal class DefaultTimeline( onNewTimelineEvents = this::onNewTimelineEvents, stateEventDataSource = stateEventDataSource, matrixCoroutineDispatchers = coroutineDispatchers, + localEchoEventFactory = localEchoEventFactory ) private var strategy: LoadTimelineStrategy = buildStrategy(LoadTimelineStrategy.Mode.Live) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index b1a3d51b36..13852a2bce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -32,6 +32,7 @@ import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.session.sync.handler.room.ReadReceiptHandler import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler @@ -55,6 +56,7 @@ internal class DefaultTimelineService @AssistedInject constructor( private val timelineEventDataSource: TimelineEventDataSource, private val clock: Clock, private val stateEventDataSource: StateEventDataSource, + private val localEchoEventFactory: LocalEchoEventFactory ) : TimelineService { @AssistedFactory @@ -82,6 +84,7 @@ internal class DefaultTimelineService @AssistedInject constructor( lightweightSettingsStorage = lightweightSettingsStorage, clock = clock, stateEventDataSource = stateEventDataSource, + localEchoEventFactory = localEchoEventFactory ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt index d81a115676..9faf301fe0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/LoadTimelineStrategy.kt @@ -43,7 +43,12 @@ import org.matrix.android.sdk.internal.database.query.findAllIncludingEvents import org.matrix.android.sdk.internal.database.query.findLastForwardChunkOfThread import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecorator +import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecoratorChain +import org.matrix.android.sdk.internal.session.room.timeline.decorator.UiEchoDecorator +import org.matrix.android.sdk.internal.session.room.timeline.decorator.UpdatedReplyDecorator import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import org.matrix.android.sdk.internal.util.time.Clock import timber.log.Timber @@ -106,6 +111,7 @@ internal class LoadTimelineStrategy constructor( val onNewTimelineEvents: (List) -> Unit, val stateEventDataSource: StateEventDataSource, val matrixCoroutineDispatchers: MatrixCoroutineDispatchers, + val localEchoEventFactory: LocalEchoEventFactory ) private var getContextLatch: CompletableDeferred? = null @@ -323,6 +329,19 @@ internal class LoadTimelineStrategy constructor( } private fun RealmResults.createTimelineChunk(): TimelineChunk? { + fun createTimelineEventDecorator(): TimelineEventDecorator { + val decorators = listOf( + UiEchoDecorator(uiEchoManager), + UpdatedReplyDecorator( + realm = dependencies.realm, + roomId = roomId, + localEchoEventFactory = dependencies.localEchoEventFactory, + timelineEventMapper = dependencies.timelineEventMapper + ) + ) + return TimelineEventDecoratorChain(decorators) + } + return firstOrNull()?.let { return TimelineChunk( chunkEntity = it, @@ -341,6 +360,9 @@ internal class LoadTimelineStrategy constructor( initialEventId = mode.originEventId(), onBuiltEvents = dependencies.onEventsUpdated, onEventsDeleted = dependencies.onEventsDeleted, + realm = dependencies.realm, + localEchoEventFactory = dependencies.localEchoEventFactory, + decorator = createTimelineEventDecorator() ) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt index 7fa36969b1..c9785e7ea1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineChunk.kt @@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.timeline import io.realm.OrderedCollectionChangeSet import io.realm.OrderedRealmCollectionChangeListener +import io.realm.Realm import io.realm.RealmConfiguration import io.realm.RealmObjectChangeListener import io.realm.RealmQuery @@ -27,9 +28,11 @@ import kotlinx.coroutines.CompletableDeferred import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.isReply 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.TimelineSettings +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.settings.LightweightSettingsStorage import org.matrix.android.sdk.internal.database.mapper.EventMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper @@ -39,10 +42,13 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetchThreadTimelineTask import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import org.matrix.android.sdk.internal.session.room.timeline.decorator.TimelineEventDecorator import org.matrix.android.sdk.internal.session.sync.handler.room.ThreadsAwarenessHandler import timber.log.Timber import java.util.Collections import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference /** * This is a wrapper around a ChunkEntity in the database. @@ -66,6 +72,9 @@ internal class TimelineChunk( private val initialEventId: String?, private val onBuiltEvents: (Boolean) -> Unit, private val onEventsDeleted: () -> Unit, + private val realm: AtomicReference, + private val decorator: TimelineEventDecorator, + val localEchoEventFactory: LocalEchoEventFactory, ) { private val isLastForward = AtomicBoolean(chunkEntity.isLastForward) @@ -74,6 +83,13 @@ internal class TimelineChunk( private var prevChunkLatch: CompletableDeferred? = null private var nextChunkLatch: CompletableDeferred? = null + /** + Map of eventId -> eventId + The key holds the eventId of the repliedTo event. + The value holds a set of eventIds of all events replying to this event. + */ + private val repliedEventsMap = HashMap>() + private val chunkObjectListener = RealmObjectChangeListener { _, changeSet -> if (changeSet == null) return@RealmObjectChangeListener if (changeSet.isDeleted.orFalse()) { @@ -353,9 +369,6 @@ internal class TimelineChunk( timelineEvents .mapIndexed { index, timelineEventEntity -> val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded() - if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { - isLastBackward.set(true) - } if (direction == Timeline.Direction.FORWARDS) { builtEventsIndexes[timelineEvent.eventId] = index builtEvents.add(index, timelineEvent) @@ -394,26 +407,45 @@ internal class TimelineChunk( } private fun TimelineEventEntity.buildAndDecryptIfNeeded(): TimelineEvent { - val timelineEvent = buildTimelineEvent(this) - val transactionId = timelineEvent.root.unsignedData?.transactionId - uiEchoManager?.onSyncedEvent(transactionId) - if (timelineEvent.isEncrypted() && - timelineEvent.root.mxDecryptionResult == null) { - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } - } - if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) { - // Thread aware for not encrypted events - timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } - } - return timelineEvent + /** + * Makes sure to update some internal state after a TimelineEvent is built. + */ + fun processTimelineEvent(timelineEvent: TimelineEvent) { + if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { + isLastBackward.set(true) + } else if (timelineEvent.root.isReply()) { + val relatesEventId = timelineEvent.getRelationContent()?.inReplyTo?.eventId + if (relatesEventId != null) { + val relatedEvents = repliedEventsMap.getOrPut(relatesEventId) { mutableSetOf() } + relatedEvents.add(timelineEvent.eventId) + } + } + val transactionId = timelineEvent.root.unsignedData?.transactionId + uiEchoManager?.onSyncedEvent(transactionId) + } + + fun decryptIfNeeded(timelineEvent: TimelineEvent) { + if (timelineEvent.isEncrypted() && + timelineEvent.root.mxDecryptionResult == null) { + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + } + if (!timelineEvent.isEncrypted() && !lightweightSettingsStorage.areThreadMessagesEnabled()) { + // Thread aware for not encrypted events + timelineEvent.root.eventId?.also { eventDecryptor.requestDecryption(TimelineEventDecryptor.DecryptionRequest(timelineEvent.root, timelineId)) } + } + } + + return buildTimelineEvent(this).also { timelineEvent -> + decryptIfNeeded(timelineEvent) + processTimelineEvent(timelineEvent) + } } private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( timelineEventEntity = eventEntity, buildReadReceipts = timelineSettings.buildReadReceipts - ).let { - // eventually enhance with ui echo? - (uiEchoManager?.decorateEventWithReactionUiEcho(it) ?: it) + ).let { timelineEvent -> + decorator.decorate(timelineEvent) } /** @@ -493,13 +525,9 @@ internal class TimelineChunk( if (!validateInsertion(range, results)) continue val newItems = results .subList(range.startIndex, range.startIndex + range.length) - .map { it.buildAndDecryptIfNeeded() } - builtEventsIndexes.entries.filter { it.value >= range.startIndex }.forEach { it.setValue(it.value + range.length) } - newItems.mapIndexed { index, timelineEvent -> - if (timelineEvent.root.type == EventType.STATE_ROOM_CREATE) { - isLastBackward.set(true) - } + newItems.mapIndexed { index, timelineEventEntity -> + val timelineEvent = timelineEventEntity.buildAndDecryptIfNeeded() val correctedIndex = range.startIndex + index builtEvents.add(correctedIndex, timelineEvent) builtEventsIndexes[timelineEvent.eventId] = correctedIndex @@ -509,11 +537,17 @@ internal class TimelineChunk( for (range in modifications) { for (modificationIndex in (range.startIndex until range.startIndex + range.length)) { val updatedEntity = results[modificationIndex] ?: continue - val builtEventIndex = builtEventsIndexes[updatedEntity.eventId] ?: continue - try { - builtEvents[builtEventIndex] = updatedEntity.buildAndDecryptIfNeeded() - } catch (failure: Throwable) { - Timber.v("Fail to update items at index: $modificationIndex") + val updatedEventId = updatedEntity.eventId + val repliesOfUpdatedEvent = repliedEventsMap.getOrElse(updatedEventId) { emptySet() }.mapNotNull { eventId -> + results.where().equalTo(TimelineEventEntityFields.EVENT_ID, eventId).findFirst() + } + repliesOfUpdatedEvent.plus(updatedEntity).forEach { entityToRebuild -> + val builtEventIndex = builtEventsIndexes[entityToRebuild.eventId] ?: return@forEach + try { + builtEvents[builtEventIndex] = entityToRebuild.buildAndDecryptIfNeeded() + } catch (failure: Throwable) { + Timber.v("Fail to update items at index: $modificationIndex") + } } } } @@ -580,7 +614,10 @@ internal class TimelineChunk( lightweightSettingsStorage = lightweightSettingsStorage, initialEventId = null, onBuiltEvents = this.onBuiltEvents, - onEventsDeleted = this.onEventsDeleted + onEventsDeleted = this.onEventsDeleted, + decorator = this.decorator, + realm = realm, + localEchoEventFactory = localEchoEventFactory ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/TimelineEventDecorator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/TimelineEventDecorator.kt new file mode 100644 index 0000000000..261407ba5f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/TimelineEventDecorator.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.timeline.decorator + +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +/** + * This interface can be used to make a copy of a TimelineEvent with new data, before the event is posted to the timeline. + */ +internal fun interface TimelineEventDecorator { + fun decorate(timelineEvent: TimelineEvent): TimelineEvent +} + +/** + * This is an implementation of [TimelineEventDecorator] which chains calls to decorators. + */ +internal class TimelineEventDecoratorChain(private val decorators: List) : TimelineEventDecorator { + + override fun decorate(timelineEvent: TimelineEvent): TimelineEvent { + var decorated = timelineEvent + val iterator = decorators.iterator() + while (iterator.hasNext()) { + val decorator = iterator.next() + decorated = decorator.decorate(decorated) + } + return decorated + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.kt new file mode 100644 index 0000000000..778a9d27d9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UiEchoDecorator.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.session.room.timeline.decorator + +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.internal.session.room.timeline.UIEchoManager + +internal class UiEchoDecorator(private val uiEchoManager: UIEchoManager?) : TimelineEventDecorator { + + override fun decorate(timelineEvent: TimelineEvent): TimelineEvent { + return uiEchoManager?.decorateEventWithReactionUiEcho(timelineEvent) ?: timelineEvent + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UpdatedReplyDecorator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UpdatedReplyDecorator.kt new file mode 100644 index 0000000000..2b12fe814c --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/decorator/UpdatedReplyDecorator.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 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.timeline.decorator + +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.isThread +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent +import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent +import org.matrix.android.sdk.api.session.room.timeline.getRelationContent +import org.matrix.android.sdk.api.session.room.timeline.isReply +import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper +import org.matrix.android.sdk.internal.database.mapper.asDomain +import org.matrix.android.sdk.internal.database.model.TimelineEventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory +import java.util.concurrent.atomic.AtomicReference + +internal class UpdatedReplyDecorator( + private val realm: AtomicReference, + private val roomId: String, + private val localEchoEventFactory: LocalEchoEventFactory, + private val timelineEventMapper: TimelineEventMapper, +) : TimelineEventDecorator { + + override fun decorate(timelineEvent: TimelineEvent): TimelineEvent { + return if (timelineEvent.isReply() && !timelineEvent.root.isThread()) { + val newRepliedEvent = createNewRepliedEvent(timelineEvent) ?: return timelineEvent + timelineEvent.copy(root = newRepliedEvent) + } else { + timelineEvent + } + } + + private fun createNewRepliedEvent(currentTimelineEvent: TimelineEvent): Event? { + val relatesEventId = currentTimelineEvent.getRelationContent()?.inReplyTo?.eventId ?: return null + val timelineEventEntity = TimelineEventEntity.where( + realm.get(), + roomId, + relatesEventId + ).findFirst() ?: return null + + val isRedactedEvent = timelineEventEntity.root?.asDomain()?.isRedacted() ?: false + + val replyText = localEchoEventFactory + .bodyForReply(currentTimelineEvent.getLastMessageContent(), true).formattedText ?: "" + + val newContent = localEchoEventFactory.createReplyTextContent( + timelineEventMapper.map(timelineEventEntity), + replyText, + null, + false, + showInThread = false, + isRedactedEvent = isRedactedEvent + ).toContent() + + val decryptionResultToSet = currentTimelineEvent.root.mxDecryptionResult?.copy( + payload = mapOf( + "content" to newContent, + "type" to EventType.MESSAGE + ) + ) + + val contentToSet = if (currentTimelineEvent.isEncrypted()) { + // Keep encrypted content as is + currentTimelineEvent.root.content + } else { + // Use new content + newContent + } + + return currentTimelineEvent.root.copyAll( + content = contentToSet, + mxDecryptionResult = decryptionResultToSet, + mCryptoError = null, + mCryptoErrorReason = null + ) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt index 05d50d9595..cb407bb1cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponseHandler.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.sync.model.RoomsSyncResponse import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.session.SessionListeners @@ -92,7 +93,7 @@ internal class SyncResponseHandler @Inject constructor( postTreatmentSyncResponse(syncResponse, isInitialSync) - markCryptoSyncCompleted(syncResponse) + markCryptoSyncCompleted(syncResponse, aggregator.cryptoStoreAggregator) handlePostSync() @@ -218,10 +219,10 @@ internal class SyncResponseHandler @Inject constructor( } } - private fun markCryptoSyncCompleted(syncResponse: SyncResponse) { + private fun markCryptoSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { relevantPlugins.measureSpan("task", "crypto_sync_handler_onSyncCompleted") { measureTimeMillis { - cryptoSyncHandler.onSyncCompleted(syncResponse) + cryptoSyncHandler.onSyncCompleted(syncResponse, cryptoStoreAggregator) }.also { Timber.v("cryptoSyncHandler.onSyncCompleted took $it ms") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt index 2b7f936fa8..af05e08da3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncResponsePostTreatmentAggregator.kt @@ -16,6 +16,8 @@ package org.matrix.android.sdk.internal.session.sync +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator + internal class SyncResponsePostTreatmentAggregator { // List of RoomId val ephemeralFilesToDelete = mutableListOf() @@ -28,4 +30,7 @@ internal class SyncResponsePostTreatmentAggregator { // Set of users to call `crossSigningService.checkTrustAndAffectedRoomShields` once per sync val userIdsForCheckingTrustAndAffectedRoomShields = mutableSetOf() + + // For the crypto store + val cryptoStoreAggregator = CryptoStoreAggregator() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt index 551db52dbd..7224b0c29c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/CryptoSyncHandler.kt @@ -29,6 +29,7 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageContent import org.matrix.android.sdk.api.session.sync.model.SyncResponse import org.matrix.android.sdk.api.session.sync.model.ToDeviceSyncResponse import org.matrix.android.sdk.internal.crypto.DefaultCryptoService +import org.matrix.android.sdk.internal.crypto.store.db.CryptoStoreAggregator import org.matrix.android.sdk.internal.crypto.tasks.toDeviceTracingId import org.matrix.android.sdk.internal.crypto.verification.DefaultVerificationService import org.matrix.android.sdk.internal.session.sync.ProgressReporter @@ -85,8 +86,8 @@ internal class CryptoSyncHandler @Inject constructor( } } - fun onSyncCompleted(syncResponse: SyncResponse) { - cryptoService.onSyncCompleted(syncResponse) + fun onSyncCompleted(syncResponse: SyncResponse, cryptoStoreAggregator: CryptoStoreAggregator) { + cryptoService.onSyncCompleted(syncResponse, cryptoStoreAggregator) } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index 4001ae2ccf..5e4886ce1e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -258,7 +258,7 @@ internal class RoomSyncHandler @Inject constructor( root = eventEntity } // Give info to crypto module - cryptoService.onStateEvent(roomId, event) + cryptoService.onStateEvent(roomId, event, aggregator.cryptoStoreAggregator) roomMemberEventHandler.handle(realm, roomId, event, isInitialSync, aggregator) } } @@ -376,8 +376,15 @@ internal class RoomSyncHandler @Inject constructor( roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) } roomTypingUsersHandler.handle(realm, roomId, null) roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE) - roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, - roomSync.unreadNotifications, roomSync.unreadThreadNotifications, aggregator = aggregator) + roomSummaryUpdater.update( + realm, + roomId, + membership, + roomSync.summary, + roomSync.unreadNotifications, + roomSync.unreadThreadNotifications, + aggregator = aggregator, + ) return roomEntity } @@ -423,7 +430,9 @@ internal class RoomSyncHandler @Inject constructor( val isInitialSync = insertType == EventInsertType.INITIAL_SYNC eventIds.add(event.eventId) - liveEventService.get().dispatchLiveEventReceived(event, roomId, isInitialSync) + if (!isInitialSync) { + liveEventService.get().dispatchLiveEventReceived(event, roomId) + } if (event.isEncrypted() && !isInitialSync) { try { @@ -486,7 +495,7 @@ internal class RoomSyncHandler @Inject constructor( } } // Give info to crypto module - cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync) + cryptoService.onLiveEvent(roomEntity.roomId, event, isInitialSync, aggregator.cryptoStoreAggregator) // Try to remove local echo event.unsignedData?.transactionId?.also { txId ->