diff --git a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt index 4ef2840e8e..a4ecc217a4 100644 --- a/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt +++ b/app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt @@ -33,6 +33,7 @@ interface SessionComponent : NodeFactoriesBindings { interface Builder { @BindsInstance fun client(matrixClient: MatrixClient): Builder + fun build(): SessionComponent } diff --git a/changelog.d/2308.bugfix b/changelog.d/2308.bugfix new file mode 100644 index 0000000000..4c553c3899 --- /dev/null +++ b/changelog.d/2308.bugfix @@ -0,0 +1 @@ +Fix 'There are multiple DataStores active for the same file' crashes diff --git a/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/SessionCoroutineScope.kt b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/SessionCoroutineScope.kt new file mode 100644 index 0000000000..10174dee78 --- /dev/null +++ b/libraries/di/src/main/kotlin/io/element/android/libraries/di/annotations/SessionCoroutineScope.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 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 io.element.android.libraries.di.annotations + +import javax.inject.Qualifier + +/** + * Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for an active session. + */ +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Qualifier +annotation class SessionCoroutineScope diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index e5ae0c9571..6fd6b362ea 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -34,12 +34,14 @@ import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import kotlinx.coroutines.CoroutineScope import java.io.Closeable interface MatrixClient : Closeable { val sessionId: SessionId val roomListService: RoomListService val mediaLoader: MatrixMediaLoader + val sessionCoroutineScope: CoroutineScope suspend fun getRoom(roomId: RoomId): MatrixRoom? suspend fun findDM(userId: UserId): RoomId? suspend fun ignoreUser(userId: UserId): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 2c6e74c96b..18b6262073 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -102,9 +102,10 @@ class RustMatrixClient( private val clock: SystemClock, ) : MatrixClient { override val sessionId: UserId = UserId(client.userId()) + override val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId") + private val innerRoomListService = syncService.roomListService() private val sessionDispatcher = dispatchers.io.limitedParallelism(64) - private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId") private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope) private val verificationService = RustSessionVerificationService(rustSyncService, sessionCoroutineScope) private val pushersService = RustPushersService( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt index 71f01f0aac..17ea8ee444 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt @@ -20,6 +20,7 @@ import com.squareup.anvil.annotations.ContributesTo import dagger.Module import dagger.Provides import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.encryption.EncryptionService import io.element.android.libraries.matrix.api.media.MatrixMediaLoader @@ -27,6 +28,7 @@ import io.element.android.libraries.matrix.api.notificationsettings.Notification import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import kotlinx.coroutines.CoroutineScope @Module @ContributesTo(SessionScope::class) @@ -60,4 +62,10 @@ object SessionMatrixModule { fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader { return matrixClient.mediaLoader } + + @SessionCoroutineScope + @Provides + fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope { + return matrixClient.sessionCoroutineScope + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 0c4704c410..1b336cfec3 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -43,10 +43,13 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.test.TestScope class FakeMatrixClient( override val sessionId: SessionId = A_SESSION_ID, + override val sessionCoroutineScope: CoroutineScope = TestScope(), private val userDisplayName: Result = Result.success(A_USER_NAME), private val userAvatarUrl: Result = Result.success(AN_AVATAR_URL), override val roomListService: RoomListService = FakeRoomListService(), diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt index 0d10d02b62..eb2fffb045 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt @@ -22,29 +22,31 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.preferencesDataStoreFile -import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.preferences.api.store.SessionPreferencesStore 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.matrix.api.user.CurrentSessionIdHolder +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import javax.inject.Inject +import java.io.File -@ContributesBinding(SessionScope::class) -@SingleIn(SessionScope::class) -class DefaultSessionPreferencesStore @Inject constructor( - @ApplicationContext context: Context, - currentSessionIdHolder: CurrentSessionIdHolder, +class DefaultSessionPreferencesStore( + context: Context, + sessionId: SessionId, + @SessionCoroutineScope sessionCoroutineScope: CoroutineScope, ) : SessionPreferencesStore { + companion object { + fun storeFile(context: Context, sessionId: SessionId): File { + val hashedUserId = sessionId.value.hash().take(16) + return context.preferencesDataStoreFile("session_${hashedUserId}_preferences") + } + } private val sendPublicReadReceiptsKey = booleanPreferencesKey("sendPublicReadReceipts") - private val hashedUserId = currentSessionIdHolder.current.value.hash().take(16) - private val dataStoreFile = context.preferencesDataStoreFile("session_${hashedUserId}_preferences") - private val store = PreferenceDataStoreFactory.create { dataStoreFile } + private val dataStoreFile = storeFile(context, sessionId) + private val store = PreferenceDataStoreFactory.create(scope = sessionCoroutineScope) { dataStoreFile } override suspend fun setSendPublicReadReceipts(enabled: Boolean) = update(sendPublicReadReceiptsKey, enabled) override fun isSendPublicReadReceiptsEnabled(): Flow = get(sendPublicReadReceiptsKey, true) diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStoreFactory.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStoreFactory.kt new file mode 100644 index 0000000000..84dcbac289 --- /dev/null +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStoreFactory.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2024 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 io.element.android.libraries.preferences.impl.store + +import android.content.Context +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 kotlinx.coroutines.CoroutineScope +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +@SingleIn(AppScope::class) +class DefaultSessionPreferencesStoreFactory @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val cache = ConcurrentHashMap() + + fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): DefaultSessionPreferencesStore = cache.getOrPut(sessionId) { + DefaultSessionPreferencesStore(context, sessionId, sessionCoroutineScope) + } +} diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt new file mode 100644 index 0000000000..36663f6628 --- /dev/null +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/SessionPreferencesModule.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2024 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 io.element.android.libraries.preferences.impl.store + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.features.preferences.api.store.SessionPreferencesStore +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder +import kotlinx.coroutines.CoroutineScope + +@Module +@ContributesTo(SessionScope::class) +object SessionPreferencesModule { + @Provides + fun providesSessionPreferencesStore( + defaultSessionPreferencesStoreFactory: DefaultSessionPreferencesStoreFactory, + currentSessionIdHolder: CurrentSessionIdHolder, + @SessionCoroutineScope sessionCoroutineScope: CoroutineScope, + ): SessionPreferencesStore { + return defaultSessionPreferencesStoreFactory + .get(currentSessionIdHolder.current, sessionCoroutineScope) + } +}