From cba3c270f51c26146e4bedcf129c846554fda20e Mon Sep 17 00:00:00 2001 From: valere Date: Mon, 5 Dec 2022 13:47:21 +0100 Subject: [PATCH] Reduce room list placeholder lags --- .../room/summary/RoomSummaryUpdater.kt | 11 +- .../timeline/RoomSummaryEventDecryptor.kt | 133 ++++++++++++++++++ .../list/home/HomeFilteredRoomsController.kt | 6 +- .../home/RoomSummaryRoomListDiffCallback.kt | 55 ++++++++ 4 files changed, 195 insertions(+), 10 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/RoomSummaryEventDecryptor.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/list/home/RoomSummaryRoomListDiffCallback.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 83c1f8a52e..55c73cc05d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -64,6 +64,7 @@ import org.matrix.android.sdk.internal.session.room.accountdata.RoomAccountDataD import org.matrix.android.sdk.internal.session.room.membership.RoomDisplayNameResolver import org.matrix.android.sdk.internal.session.room.membership.RoomMemberHelper import org.matrix.android.sdk.internal.session.room.relationship.RoomChildRelationInfo +import org.matrix.android.sdk.internal.session.room.timeline.RoomSummaryEventDecryptor import org.matrix.android.sdk.internal.session.sync.SyncResponsePostTreatmentAggregator import timber.log.Timber import javax.inject.Inject @@ -73,10 +74,9 @@ internal class RoomSummaryUpdater @Inject constructor( @UserId private val userId: String, private val roomDisplayNameResolver: RoomDisplayNameResolver, private val roomAvatarResolver: RoomAvatarResolver, - private val eventDecryptor: EventDecryptor, -// private val crossSigningService: DefaultCrossSigningService, private val roomAccountDataDataSource: RoomAccountDataDataSource, private val homeServerCapabilitiesService: HomeServerCapabilitiesService, + private val roomSummaryEventDecryptor: RoomSummaryEventDecryptor ) { fun refreshLatestPreviewContent(realm: Realm, roomId: String) { @@ -215,12 +215,7 @@ internal class RoomSummaryUpdater @Inject constructor( Timber.v("Decryption skipped due to missing root event $eventId") } else -> { - if (root.type == EventType.ENCRYPTED && root.decryptionResultJson == null) { - Timber.v("Should decrypt $eventId") - tryOrNull { - runBlocking { eventDecryptor.decryptEvent(root.asDomain(), "") } - }?.let { root.setDecryptionResult(it) } - } + roomSummaryEventDecryptor.requestDecryption(root.asDomain()) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/RoomSummaryEventDecryptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/RoomSummaryEventDecryptor.kt new file mode 100644 index 0000000000..c2692e2a48 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/RoomSummaryEventDecryptor.kt @@ -0,0 +1,133 @@ +/* + * 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 + +import com.zhuinden.monarchy.Monarchy +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixCoroutineDispatchers +import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM +import org.matrix.android.sdk.api.session.crypto.CryptoService +import org.matrix.android.sdk.api.session.crypto.MXCryptoError +import org.matrix.android.sdk.api.session.crypto.NewSessionListener +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.internal.database.model.EventEntity +import org.matrix.android.sdk.internal.database.query.where +import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.SessionScope +import timber.log.Timber +import javax.inject.Inject + +@SessionScope +internal class RoomSummaryEventDecryptor @Inject constructor( + @SessionDatabase private val monarchy: Monarchy, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope, + private val cryptoService: dagger.Lazy +) { + + internal sealed class Message { + data class DecryptEvent(val event: Event) : Message() + data class NewSessionImported(val sessionId: String) : Message() + } + + private val scope: CoroutineScope = CoroutineScope( + cryptoCoroutineScope.coroutineContext + + SupervisorJob() + + CoroutineName("RoomSummaryDecryptor") + ) + + private val channel = Channel(capacity = 300) + + private val newSessionListener = object : NewSessionListener { + override fun onNewSession(roomId: String?, sessionId: String) { + scope.launch(coroutineDispatchers.computation) { + channel.send(Message.NewSessionImported(sessionId)) + } + } + } + + private val unknownSessionsFailure = mutableMapOf>() + + init { + scope.launch { + cryptoService.get().addNewSessionListener(newSessionListener) + for (request in channel) { + when (request) { + is Message.DecryptEvent -> handleDecryptEvent(request.event) + is Message.NewSessionImported -> handleNewSessionImported(request.sessionId) + } + } + } + } + + private fun handleNewSessionImported(sessionId: String) { + unknownSessionsFailure[sessionId] + ?.toList() + .orEmpty() + .also { + unknownSessionsFailure[sessionId]?.clear() + }.forEach { + // post a retry! + requestDecryption(it) + } + } + + private suspend fun handleDecryptEvent(event: Event) { + if (event.getClearType() != EventType.ENCRYPTED) return + val algorithm = event.content?.get("algorithm") as? String + if (algorithm != MXCRYPTO_ALGORITHM_MEGOLM) return + + try { + val result = cryptoService.get().decryptEvent(event, "") + // now let's persist the result in database + monarchy.writeAsync { realm -> + val eventEntity = EventEntity.where(realm, event.eventId.orEmpty()).findFirst() + eventEntity?.setDecryptionResult(result) + } + } catch (failure: Throwable) { + Timber.v(failure, "Failed to decrypt event ${event.eventId}") + // We don't need to get more details, just mark this session in failures + if (failure is MXCryptoError.Base) { + monarchy.writeAsync { realm -> + EventEntity.where(realm, eventId = event.eventId.orEmpty()) + .findFirst() + ?.let { + it.decryptionErrorCode = failure.errorType.name + it.decryptionErrorReason = failure.technicalMessage.takeIf { it.isNotEmpty() } ?: failure.detailedErrorDescription + } + } + + if (failure.errorType == MXCryptoError.ErrorType.UNKNOWN_INBOUND_SESSION_ID + || failure.errorType == MXCryptoError.ErrorType.UNKNOWN_MESSAGE_INDEX) { + (event.content["session_id"] as? String)?.let { sessionId -> + unknownSessionsFailure.getOrPut(sessionId) { mutableSetOf() } + .add(event) + } + } + } + } + } + + fun requestDecryption(event: Event) { + channel.trySend(Message.DecryptEvent(event)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt index 500039e3eb..52c319089c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/HomeFilteredRoomsController.kt @@ -32,10 +32,12 @@ import javax.inject.Inject class HomeFilteredRoomsController @Inject constructor( private val roomSummaryItemFactory: RoomSummaryItemFactory, - fontScalePreferences: FontScalePreferences + fontScalePreferences: FontScalePreferences, + roomSummaryRoomListDiffCallback: RoomSummaryRoomListDiffCallback, ) : PagedListEpoxyController( // Important it must match the PageList builder notify Looper - modelBuildingHandler = createUIHandler() + modelBuildingHandler = createUIHandler(), + itemDiffCallback = roomSummaryRoomListDiffCallback, ) { private var roomChangeMembershipStates: Map? = null diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomSummaryRoomListDiffCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomSummaryRoomListDiffCallback.kt new file mode 100644 index 0000000000..b24118b81b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/list/home/RoomSummaryRoomListDiffCallback.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.home.room.list.home + +import androidx.recyclerview.widget.DiffUtil +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import javax.inject.Inject + +class RoomSummaryRoomListDiffCallback @Inject constructor( + vectorPreferences: VectorPreferences +): DiffUtil.ItemCallback() { + + override fun areItemsTheSame(oldItem: RoomSummary, newItem: RoomSummary): Boolean { + return oldItem.roomId == newItem.roomId + } + + override fun areContentsTheSame(oldItem: RoomSummary, newItem: RoomSummary): Boolean { + // for this use case we can test less things + if (oldItem.roomId != newItem.roomId) return false + if (oldItem.displayName != newItem.displayName) return false + if (oldItem.name != newItem.name) return false + if (oldItem.topic != newItem.topic) return false + if (oldItem.avatarUrl != newItem.avatarUrl) return false + if (oldItem.canonicalAlias != newItem.canonicalAlias) return false + if (oldItem.aliases != newItem.aliases) return false + if (oldItem.isDirect != newItem.isDirect) return false + if (oldItem.directUserPresence != newItem.directUserPresence) return false + if (oldItem.latestPreviewableEvent != newItem.latestPreviewableEvent) return false + if (oldItem.notificationCount != newItem.notificationCount) return false + if (oldItem.highlightCount != newItem.highlightCount) return false + if (oldItem.threadNotificationCount != newItem.threadNotificationCount) return false + if (oldItem.threadHighlightCount != newItem.threadHighlightCount) return false + if (oldItem.hasUnreadMessages != newItem.hasUnreadMessages) return false + if (oldItem.userDrafts != newItem.userDrafts) return false + if (oldItem.isEncrypted != newItem.isEncrypted) return false + if (oldItem.typingUsers != newItem.typingUsers) return false + if (oldItem.hasFailedSending != newItem.hasFailedSending) return false + return true + } +}