From dbb96c5212f22d3f362a737dac185d6312ac6ad7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 11 Apr 2025 15:37:44 +0200 Subject: [PATCH] Ensure that we have only one single instance of SeenInviteStore per session (#4577) * Ensure that we have only one single instance of SeenInviteStore per session. Fixes #4558 Fix crash: java.lang.IllegalStateException: There are multiple DataStores active for the same file: /data/user/0/io.element.android.x/files/datastore/session_0ebb139587b6d940_seen-invites.preferences_pb. You should either maintain your DataStore as a singleton or confirm that there is no two DataStore's active on the same file (by confirming that the scope is cancelled). * Inject the SeenInvitesStore to reduce boilerplate code. --- .../invite/impl/DefaultSeenInvitesStore.kt | 19 ++------ .../impl/DefaultSeenInvitesStoreFactory.kt | 44 +++++++++++++++++++ .../invite/impl/SeenInvitesStoreFactory.kt | 19 ++++++++ .../features/invite/impl/di/InviteModule.kt | 17 +++++++ 4 files changed, 84 insertions(+), 15 deletions(-) create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt create mode 100644 features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt index 3accc163b0..38295daa27 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStore.kt @@ -12,36 +12,25 @@ import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStoreFile -import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.androidutils.hash.hash -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject private val seenInvitesKey = stringSetPreferencesKey("seenInvites") -@SingleIn(SessionScope::class) -@ContributesBinding(SessionScope::class) -class DefaultSeenInvitesStore @Inject constructor( - @ApplicationContext context: Context, - currentSessionIdHolder: CurrentSessionIdHolder, - @SessionCoroutineScope sessionCoroutineScope: CoroutineScope, +class DefaultSeenInvitesStore( + context: Context, + sessionId: SessionId, + sessionCoroutineScope: CoroutineScope, sessionObserver: SessionObserver, ) : SeenInvitesStore { - private val sessionId: SessionId = currentSessionIdHolder.current - init { sessionObserver.addListener(object : SessionListener { override suspend fun onSessionCreated(userId: String) = Unit diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt new file mode 100644 index 0000000000..256214e5d2 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/DefaultSeenInvitesStoreFactory.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl + +import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.sessionstorage.api.observer.SessionObserver +import kotlinx.coroutines.CoroutineScope +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class DefaultSeenInvitesStoreFactory @Inject constructor( + @ApplicationContext private val context: Context, + private val sessionObserver: SessionObserver, +) : SeenInvitesStoreFactory { + // We can have only one class accessing a single data store, so keep a cache of them. + private val cache = ConcurrentHashMap() + + override fun getOrCreate( + sessionId: SessionId, + sessionCoroutineScope: CoroutineScope, + ): SeenInvitesStore { + return cache.getOrPut(sessionId) { + DefaultSeenInvitesStore( + context = context, + sessionId = sessionId, + sessionCoroutineScope = sessionCoroutineScope, + sessionObserver = sessionObserver, + ) + } + } +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt new file mode 100644 index 0000000000..4d681e8462 --- /dev/null +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/SeenInvitesStoreFactory.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2025 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.invite.impl + +import io.element.android.features.invite.api.SeenInvitesStore +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.CoroutineScope + +interface SeenInvitesStoreFactory { + fun getOrCreate( + sessionId: SessionId, + sessionCoroutineScope: CoroutineScope, + ): SeenInvitesStore +} diff --git a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt index 6a117b621b..a9308ca0fc 100644 --- a/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt +++ b/features/invite/impl/src/main/kotlin/io/element/android/features/invite/impl/di/InviteModule.kt @@ -10,14 +10,31 @@ package io.element.android.features.invite.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module +import dagger.Provides +import io.element.android.features.invite.api.SeenInvitesStore import io.element.android.features.invite.api.response.AcceptDeclineInviteState +import io.element.android.features.invite.impl.SeenInvitesStoreFactory import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.MatrixClient @ContributesTo(SessionScope::class) @Module interface InviteModule { @Binds fun bindAcceptDeclinePresenter(presenter: AcceptDeclineInvitePresenter): Presenter + + companion object { + @Provides + fun providesSeenInvitesStore( + factory: SeenInvitesStoreFactory, + matrixClient: MatrixClient, + ): SeenInvitesStore { + return factory.getOrCreate( + matrixClient.sessionId, + matrixClient.sessionCoroutineScope, + ) + } + } }