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

@ -7,32 +7,26 @@
package io.element.android.features.rageshake.impl.crash
import android.content.Context
import androidx.datastore.core.DataStore
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 com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_crash")
private val appHasCrashedKey = booleanPreferencesKey("appHasCrashed")
private val crashDataKey = stringPreferencesKey("crashData")
@ContributesBinding(AppScope::class)
class PreferencesCrashDataStore @Inject constructor(
@ApplicationContext context: Context
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : CrashDataStore {
private val store = context.dataStore
private val store = preferenceDataStoreFactory.create("elementx_crash")
override fun setCrashData(crashData: String) {
// Must block

View file

@ -7,7 +7,6 @@
package io.element.android.features.rageshake.impl.crash
import android.content.Context
import android.os.Build
import io.element.android.libraries.core.data.tryOrNull
import timber.log.Timber
@ -15,9 +14,8 @@ import java.io.PrintWriter
import java.io.StringWriter
class VectorUncaughtExceptionHandler(
context: Context
private val preferencesCrashDataStore: PreferencesCrashDataStore,
) : Thread.UncaughtExceptionHandler {
private val crashDataStore = PreferencesCrashDataStore(context)
private var previousHandler: Thread.UncaughtExceptionHandler? = null
/**
@ -65,7 +63,7 @@ class VectorUncaughtExceptionHandler(
append(sw.buffer.toString())
}
Timber.e("FATAL EXCEPTION $bugDescription")
crashDataStore.setCrashData(bugDescription)
preferencesCrashDataStore.setCrashData(bugDescription)
// Show the classical system popup
previousHandler?.uncaughtException(thread, throwable)
}

View file

@ -0,0 +1,17 @@
/*
* 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.rageshake.impl.di
import com.squareup.anvil.annotations.ContributesTo
import io.element.android.features.rageshake.impl.crash.PreferencesCrashDataStore
import io.element.android.libraries.di.AppScope
@ContributesTo(AppScope::class)
interface RageshakeBindings {
fun preferencesCrashDataStore(): PreferencesCrashDataStore
}

View file

@ -7,31 +7,25 @@
package io.element.android.features.rageshake.impl.rageshake
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_rageshake")
private val enabledKey = booleanPreferencesKey("enabled")
private val sensitivityKey = floatPreferencesKey("sensitivity")
@ContributesBinding(AppScope::class)
class PreferencesRageshakeDataStore @Inject constructor(
@ApplicationContext context: Context
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : RageshakeDataStore {
private val store = context.dataStore
private val store = preferenceDataStoreFactory.create("elementx_rageshake")
override fun isEnabled(): Flow<Boolean> {
return store.data.map { prefs ->

View file

@ -9,28 +9,28 @@ package io.element.android.features.rageshake.impl.crash
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class VectorUncaughtExceptionHandlerTest {
@Test
fun `activate should change the default handler`() {
val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
val sut = VectorUncaughtExceptionHandler(PreferencesCrashDataStore(FakePreferenceDataStoreFactory()))
sut.activate()
assertThat(Thread.getDefaultUncaughtExceptionHandler()).isInstanceOf(VectorUncaughtExceptionHandler::class.java)
}
@Test
fun `uncaught exception`() = runTest {
val crashDataStore = PreferencesCrashDataStore(RuntimeEnvironment.getApplication())
val crashDataStore = PreferencesCrashDataStore(FakePreferenceDataStoreFactory())
assertThat(crashDataStore.appHasCrashed().first()).isFalse()
assertThat(crashDataStore.crashInfo().first()).isEmpty()
val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
val sut = VectorUncaughtExceptionHandler(crashDataStore)
sut.uncaughtException(Thread(), AN_EXCEPTION)
assertThat(crashDataStore.appHasCrashed().first()).isTrue()
val crashInfo = crashDataStore.crashInfo().first()