From 741f9fabbb1b2ff0af6cacc5b7f56abcc8d93636 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 1 Feb 2022 13:02:41 +0000 Subject: [PATCH] providing a way to lazily read dynamic datastore instances - fixes crash where multiple session store instances attempt to read from the same datastore backing file --- .../app/core/datastore/DataStoreProvider.kt | 50 +++++++++++++++++++ .../im/vector/app/core/extensions/Context.kt | 5 ++ .../features/session/VectorSessionStore.kt | 18 +++---- 3 files changed, 63 insertions(+), 10 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/datastore/DataStoreProvider.kt diff --git a/vector/src/main/java/im/vector/app/core/datastore/DataStoreProvider.kt b/vector/src/main/java/im/vector/app/core/datastore/DataStoreProvider.kt new file mode 100644 index 0000000000..0cea36f53d --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/datastore/DataStoreProvider.kt @@ -0,0 +1,50 @@ +/* + * 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.core.datastore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile +import java.util.concurrent.ConcurrentHashMap +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/** + * Provides a singleton datastore cache + * allows for lazily fetching a datastore instance by key to avoid creating multiple stores for the same file + */ +fun dataStoreProvider(): ReadOnlyProperty DataStore> { + return MappedPreferenceDataStoreSingletonDelegate() +} + +private class MappedPreferenceDataStoreSingletonDelegate : ReadOnlyProperty DataStore> { + + private val dataStoreCache = ConcurrentHashMap>() + private val provider: (Context) -> (String) -> DataStore = { context -> + { key -> + dataStoreCache.getOrPut(key) { + PreferenceDataStoreFactory.create { + context.preferencesDataStoreFile(key) + } + } + } + } + + override fun getValue(thisRef: Context, property: KProperty<*>) = provider.invoke(thisRef) +} diff --git a/vector/src/main/java/im/vector/app/core/extensions/Context.kt b/vector/src/main/java/im/vector/app/core/extensions/Context.kt index 1063d30a41..b8b367b740 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/Context.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/Context.kt @@ -23,7 +23,10 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.FloatRange import androidx.core.content.ContextCompat +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences import dagger.hilt.EntryPoints +import im.vector.app.core.datastore.dataStoreProvider import im.vector.app.core.di.SingletonEntryPoint import kotlin.math.roundToInt @@ -50,3 +53,5 @@ fun Context.getTintedDrawable(@DrawableRes drawableRes: Int, private fun Float.toAndroidAlpha(): Int { return (this * 255).roundToInt() } + +val Context.dataStoreProvider: (String) -> DataStore by dataStoreProvider() diff --git a/vector/src/main/java/im/vector/app/features/session/VectorSessionStore.kt b/vector/src/main/java/im/vector/app/features/session/VectorSessionStore.kt index ce85eeeb98..a2f3196979 100644 --- a/vector/src/main/java/im/vector/app/features/session/VectorSessionStore.kt +++ b/vector/src/main/java/im/vector/app/features/session/VectorSessionStore.kt @@ -17,44 +17,42 @@ package im.vector.app.features.session import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore +import im.vector.app.core.extensions.dataStoreProvider import im.vector.app.features.onboarding.FtueUseCase import kotlinx.coroutines.flow.first import org.matrix.android.sdk.internal.util.md5 /** - * Local storage for: + * User session scoped storage for: * - messaging use case (Enum/String) */ class VectorSessionStore constructor( - private val context: Context, + context: Context, myUserId: String ) { - private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_session_store_${myUserId.md5()}") private val useCaseKey = stringPreferencesKey("use_case") + private val dataStore by lazy { context.dataStoreProvider("vector_session_store_${myUserId.md5()}") } - suspend fun readUseCase() = context.dataStore.data.first().let { preferences -> + suspend fun readUseCase() = dataStore.data.first().let { preferences -> preferences[useCaseKey]?.let { FtueUseCase.from(it) } } suspend fun setUseCase(useCase: FtueUseCase) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[useCaseKey] = useCase.persistableValue } } suspend fun resetUseCase() { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings.remove(useCaseKey) } } suspend fun clear() { - context.dataStore.edit { settings -> settings.clear() } + dataStore.edit { settings -> settings.clear() } } }