Handle preference stores corruption by clearing them (#5086)

* Handle preference stores corruption by clearing them:
    - Use the centralised `PreferenceDataStoreFactory` instead of `preferences by`.
    - Add `DefaultPreferencesCorruptionHandlerFactory.replaceWithEmpty` to its `create(name)` method so all preference stores are cleared if they're corrupted.

* Add detekt rule to make sure we use `PreferenceDataStoreFactory` instead of `by preferencesDataStore`

* Remove `@SingleIn` annotations as the annotated class no longer have to be singletons
This commit is contained in:
Jorge Martin Espinosa 2025-08-22 08:59:06 +02:00 committed by GitHub
parent 3faaab407f
commit 8245ad8bc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 198 additions and 138 deletions

View file

@ -27,6 +27,7 @@ dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.pushstore.api)
implementation(libs.androidx.corektx)
implementation(libs.androidx.datastore.preferences)
@ -38,6 +39,7 @@ dependencies {
testImplementation(libs.test.turbine)
testImplementation(libs.coroutines.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.services.appnavstate.test)
testImplementation(projects.libraries.pushstore.test)

View file

@ -13,6 +13,7 @@ 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.preferences.api.store.PreferenceDataStoreFactory
import io.element.android.libraries.pushstore.api.UserPushStore
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
import java.util.concurrent.ConcurrentHashMap
@ -22,6 +23,7 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultUserPushStoreFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : UserPushStoreFactory {
// We can have only one class accessing a single data store, so keep a cache of them.
private val cache = ConcurrentHashMap<SessionId, UserPushStore>()
@ -29,7 +31,8 @@ class DefaultUserPushStoreFactory @Inject constructor(
return cache.getOrPut(userId) {
UserPushStoreDataStore(
context = context,
userId = userId
userId = userId,
factory = preferenceDataStoreFactory,
)
}
}

View file

@ -13,12 +13,12 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import androidx.datastore.preferences.preferencesDataStoreFile
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.bool.orTrue
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import io.element.android.libraries.pushstore.api.UserPushStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@ -31,6 +31,7 @@ import timber.log.Timber
class UserPushStoreDataStore(
private val context: Context,
userId: SessionId,
factory: PreferenceDataStoreFactory,
) : UserPushStore {
// Hash the sessionId to get rid of exotic chars and take only the first 16 chars.
// The risk of collision is not high.
@ -49,28 +50,28 @@ class UserPushStoreDataStore(
}
}
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = preferenceName)
private val store: DataStore<Preferences> = factory.create(preferenceName)
private val pushProviderName = stringPreferencesKey("pushProviderName")
private val currentPushKey = stringPreferencesKey("currentPushKey")
private val notificationEnabled = booleanPreferencesKey("notificationEnabled")
private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError")
override suspend fun getPushProviderName(): String? {
return context.dataStore.data.first()[pushProviderName]
return store.data.first()[pushProviderName]
}
override suspend fun setPushProviderName(value: String) {
context.dataStore.edit {
store.edit {
it[pushProviderName] = value
}
}
override suspend fun getCurrentRegisteredPushKey(): String? {
return context.dataStore.data.first()[currentPushKey]
return store.data.first()[currentPushKey]
}
override suspend fun setCurrentRegisteredPushKey(value: String?) {
context.dataStore.edit {
store.edit {
if (value == null) {
it.remove(currentPushKey)
} else {
@ -80,11 +81,11 @@ class UserPushStoreDataStore(
}
override fun getNotificationEnabledForDevice(): Flow<Boolean> {
return context.dataStore.data.map { it[notificationEnabled].orTrue() }
return store.data.map { it[notificationEnabled].orTrue() }
}
override suspend fun setNotificationEnabledForDevice(enabled: Boolean) {
context.dataStore.edit {
store.edit {
it[notificationEnabled] = enabled
}
}
@ -94,17 +95,17 @@ class UserPushStoreDataStore(
}
override fun ignoreRegistrationError(): Flow<Boolean> {
return context.dataStore.data.map { it[ignoreRegistrationError].orFalse() }
return store.data.map { it[ignoreRegistrationError].orFalse() }
}
override suspend fun setIgnoreRegistrationError(ignore: Boolean) {
context.dataStore.edit {
store.edit {
it[ignoreRegistrationError] = ignore
}
}
override suspend fun reset() {
context.dataStore.edit {
store.edit {
it.clear()
}
// Also delete the file

View file

@ -7,44 +7,40 @@
package io.element.android.libraries.pushstore.impl.clientsecret
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 com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
import kotlinx.coroutines.flow.first
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_client_secret_store")
@ContributesBinding(AppScope::class)
class DataStorePushClientSecretStore @Inject constructor(
@ApplicationContext private val context: Context,
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : PushClientSecretStore {
private val dataStore = preferenceDataStoreFactory.create("push_client_secret_store")
override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
context.dataStore.edit { settings ->
dataStore.edit { settings ->
settings[getPreferenceKeyForUser(userId)] = clientSecret
}
}
override suspend fun getSecret(userId: SessionId): String? {
return context.dataStore.data.first()[getPreferenceKeyForUser(userId)]
return dataStore.data.first()[getPreferenceKeyForUser(userId)]
}
override suspend fun resetSecret(userId: SessionId) {
context.dataStore.edit { settings ->
dataStore.edit { settings ->
settings.remove(getPreferenceKeyForUser(userId))
}
}
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
val keyValues = context.dataStore.data.first().asMap()
val keyValues = dataStore.data.first().asMap()
val matchingKey = keyValues.keys.find {
keyValues[it] == clientSecret
}

View file

@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -92,5 +93,6 @@ class UserPushStoreDataStoreTest {
) = UserPushStoreDataStore(
context = InstrumentationRegistry.getInstrumentation().context,
userId = sessionId,
factory = FakePreferenceDataStoreFactory(),
)
}