Skip to content

Commit ede1dc0

Browse files
authored
Fix crash about several DataStores using the same file (#2312)
* Fix crash about several DataStores using the same file - Create `@SessionCoroutineScope` annotation to pass a session-managed coroutine scope to the DI. - Expose this scope from `MatrixClient`. - Rework DataStore file creation a bit. - Centralise session preference creation through `DefaultSessionPreferencesStoreFactory` until we figure out what went wrong with the scoping
1 parent 7636939 commit ede1dc0

File tree

10 files changed

+137
-15
lines changed

10 files changed

+137
-15
lines changed

app/src/main/kotlin/io/element/android/x/di/SessionComponent.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface SessionComponent : NodeFactoriesBindings {
3333
interface Builder {
3434
@BindsInstance
3535
fun client(matrixClient: MatrixClient): Builder
36+
3637
fun build(): SessionComponent
3738
}
3839

changelog.d/2308.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix 'There are multiple DataStores active for the same file' crashes
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.di.annotations
18+
19+
import javax.inject.Qualifier
20+
21+
/**
22+
* Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for an active session.
23+
*/
24+
@Retention(AnnotationRetention.RUNTIME)
25+
@MustBeDocumented
26+
@Qualifier
27+
annotation class SessionCoroutineScope

libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ import io.element.android.libraries.matrix.api.sync.SyncService
3434
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
3535
import io.element.android.libraries.matrix.api.user.MatrixUser
3636
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
37+
import kotlinx.coroutines.CoroutineScope
3738
import java.io.Closeable
3839

3940
interface MatrixClient : Closeable {
4041
val sessionId: SessionId
4142
val roomListService: RoomListService
4243
val mediaLoader: MatrixMediaLoader
44+
val sessionCoroutineScope: CoroutineScope
4345
suspend fun getRoom(roomId: RoomId): MatrixRoom?
4446
suspend fun findDM(userId: UserId): RoomId?
4547
suspend fun ignoreUser(userId: UserId): Result<Unit>

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,9 +102,10 @@ class RustMatrixClient(
102102
private val clock: SystemClock,
103103
) : MatrixClient {
104104
override val sessionId: UserId = UserId(client.userId())
105+
override val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId")
106+
105107
private val innerRoomListService = syncService.roomListService()
106108
private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
107-
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId")
108109
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
109110
private val verificationService = RustSessionVerificationService(rustSyncService, sessionCoroutineScope)
110111
private val pushersService = RustPushersService(

libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/di/SessionMatrixModule.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import com.squareup.anvil.annotations.ContributesTo
2020
import dagger.Module
2121
import dagger.Provides
2222
import io.element.android.libraries.di.SessionScope
23+
import io.element.android.libraries.di.annotations.SessionCoroutineScope
2324
import io.element.android.libraries.matrix.api.MatrixClient
2425
import io.element.android.libraries.matrix.api.encryption.EncryptionService
2526
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
2627
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
2728
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
2829
import io.element.android.libraries.matrix.api.roomlist.RoomListService
2930
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
31+
import kotlinx.coroutines.CoroutineScope
3032

3133
@Module
3234
@ContributesTo(SessionScope::class)
@@ -60,4 +62,10 @@ object SessionMatrixModule {
6062
fun provideMediaLoader(matrixClient: MatrixClient): MatrixMediaLoader {
6163
return matrixClient.mediaLoader
6264
}
65+
66+
@SessionCoroutineScope
67+
@Provides
68+
fun provideSessionCoroutineScope(matrixClient: MatrixClient): CoroutineScope {
69+
return matrixClient.sessionCoroutineScope
70+
}
6371
}

libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@ import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
4343
import io.element.android.libraries.matrix.test.sync.FakeSyncService
4444
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
4545
import io.element.android.tests.testutils.simulateLongTask
46+
import kotlinx.coroutines.CoroutineScope
4647
import kotlinx.coroutines.delay
48+
import kotlinx.coroutines.test.TestScope
4749

4850
class FakeMatrixClient(
4951
override val sessionId: SessionId = A_SESSION_ID,
52+
override val sessionCoroutineScope: CoroutineScope = TestScope(),
5053
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
5154
private val userAvatarUrl: Result<String> = Result.success(AN_AVATAR_URL),
5255
override val roomListService: RoomListService = FakeRoomListService(),

libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultSessionPreferencesStore.kt

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,31 @@ import androidx.datastore.preferences.core.Preferences
2222
import androidx.datastore.preferences.core.booleanPreferencesKey
2323
import androidx.datastore.preferences.core.edit
2424
import androidx.datastore.preferences.preferencesDataStoreFile
25-
import com.squareup.anvil.annotations.ContributesBinding
2625
import io.element.android.features.preferences.api.store.SessionPreferencesStore
2726
import io.element.android.libraries.androidutils.file.safeDelete
2827
import io.element.android.libraries.androidutils.hash.hash
29-
import io.element.android.libraries.di.ApplicationContext
30-
import io.element.android.libraries.di.SessionScope
31-
import io.element.android.libraries.di.SingleIn
32-
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
28+
import io.element.android.libraries.di.annotations.SessionCoroutineScope
29+
import io.element.android.libraries.matrix.api.core.SessionId
30+
import kotlinx.coroutines.CoroutineScope
3331
import kotlinx.coroutines.flow.Flow
3432
import kotlinx.coroutines.flow.map
35-
import javax.inject.Inject
33+
import java.io.File
3634

37-
@ContributesBinding(SessionScope::class)
38-
@SingleIn(SessionScope::class)
39-
class DefaultSessionPreferencesStore @Inject constructor(
40-
@ApplicationContext context: Context,
41-
currentSessionIdHolder: CurrentSessionIdHolder,
35+
class DefaultSessionPreferencesStore(
36+
context: Context,
37+
sessionId: SessionId,
38+
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
4239
) : SessionPreferencesStore {
40+
companion object {
41+
fun storeFile(context: Context, sessionId: SessionId): File {
42+
val hashedUserId = sessionId.value.hash().take(16)
43+
return context.preferencesDataStoreFile("session_${hashedUserId}_preferences")
44+
}
45+
}
4346
private val sendPublicReadReceiptsKey = booleanPreferencesKey("sendPublicReadReceipts")
44-
private val hashedUserId = currentSessionIdHolder.current.value.hash().take(16)
4547

46-
private val dataStoreFile = context.preferencesDataStoreFile("session_${hashedUserId}_preferences")
47-
private val store = PreferenceDataStoreFactory.create { dataStoreFile }
48+
private val dataStoreFile = storeFile(context, sessionId)
49+
private val store = PreferenceDataStoreFactory.create(scope = sessionCoroutineScope) { dataStoreFile }
4850

4951
override suspend fun setSendPublicReadReceipts(enabled: Boolean) = update(sendPublicReadReceiptsKey, enabled)
5052
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = get(sendPublicReadReceiptsKey, true)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.preferences.impl.store
18+
19+
import android.content.Context
20+
import io.element.android.libraries.di.AppScope
21+
import io.element.android.libraries.di.ApplicationContext
22+
import io.element.android.libraries.di.SingleIn
23+
import io.element.android.libraries.matrix.api.core.SessionId
24+
import kotlinx.coroutines.CoroutineScope
25+
import java.util.concurrent.ConcurrentHashMap
26+
import javax.inject.Inject
27+
28+
@SingleIn(AppScope::class)
29+
class DefaultSessionPreferencesStoreFactory @Inject constructor(
30+
@ApplicationContext private val context: Context,
31+
) {
32+
private val cache = ConcurrentHashMap<SessionId, DefaultSessionPreferencesStore>()
33+
34+
fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): DefaultSessionPreferencesStore = cache.getOrPut(sessionId) {
35+
DefaultSessionPreferencesStore(context, sessionId, sessionCoroutineScope)
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.element.android.libraries.preferences.impl.store
18+
19+
import com.squareup.anvil.annotations.ContributesTo
20+
import dagger.Module
21+
import dagger.Provides
22+
import io.element.android.features.preferences.api.store.SessionPreferencesStore
23+
import io.element.android.libraries.di.SessionScope
24+
import io.element.android.libraries.di.annotations.SessionCoroutineScope
25+
import io.element.android.libraries.matrix.api.user.CurrentSessionIdHolder
26+
import kotlinx.coroutines.CoroutineScope
27+
28+
@Module
29+
@ContributesTo(SessionScope::class)
30+
object SessionPreferencesModule {
31+
@Provides
32+
fun providesSessionPreferencesStore(
33+
defaultSessionPreferencesStoreFactory: DefaultSessionPreferencesStoreFactory,
34+
currentSessionIdHolder: CurrentSessionIdHolder,
35+
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
36+
): SessionPreferencesStore {
37+
return defaultSessionPreferencesStoreFactory
38+
.get(currentSessionIdHolder.current, sessionCoroutineScope)
39+
}
40+
}

0 commit comments

Comments
 (0)