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:
parent
3faaab407f
commit
8245ad8bc3
30 changed files with 198 additions and 138 deletions
|
|
@ -27,6 +27,7 @@ dependencies {
|
|||
implementation(projects.appconfig)
|
||||
implementation(projects.features.enterprise.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
|
|
|
|||
|
|
@ -7,44 +7,39 @@
|
|||
|
||||
package io.element.android.features.lockscreen.impl.storage
|
||||
|
||||
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.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.lockscreen.impl.LockScreenConfig
|
||||
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.preferences.api.store.PreferenceDataStoreFactory
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "pin_code_store")
|
||||
|
||||
@SingleIn(AppScope::class)
|
||||
@ContributesBinding(AppScope::class)
|
||||
class PreferencesLockScreenStore @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||
private val lockScreenConfig: LockScreenConfig,
|
||||
) : LockScreenStore {
|
||||
private val dataStore = preferenceDataStoreFactory.create("pin_code_store")
|
||||
|
||||
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
|
||||
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
|
||||
private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled")
|
||||
|
||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
||||
return context.dataStore.data.map { preferences ->
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences.getRemainingPinCodeAttemptsNumber()
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun onWrongPin() {
|
||||
context.dataStore.edit { preferences ->
|
||||
dataStore.edit { preferences ->
|
||||
val current = preferences.getRemainingPinCodeAttemptsNumber()
|
||||
val remaining = (current - 1).coerceAtLeast(0)
|
||||
preferences[remainingAttemptsKey] = remaining
|
||||
|
|
@ -52,43 +47,43 @@ class PreferencesLockScreenStore @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun resetCounter() {
|
||||
context.dataStore.edit { preferences ->
|
||||
dataStore.edit { preferences ->
|
||||
preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getEncryptedCode(): String? {
|
||||
return context.dataStore.data.map { preferences ->
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[pinCodeKey]
|
||||
}.first()
|
||||
}
|
||||
|
||||
override suspend fun saveEncryptedPinCode(pinCode: String) {
|
||||
context.dataStore.edit { preferences ->
|
||||
dataStore.edit { preferences ->
|
||||
preferences[pinCodeKey] = pinCode
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteEncryptedPinCode() {
|
||||
context.dataStore.edit { preferences ->
|
||||
dataStore.edit { preferences ->
|
||||
preferences.remove(pinCodeKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasPinCode(): Flow<Boolean> {
|
||||
return context.dataStore.data.map { preferences ->
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[pinCodeKey] != null
|
||||
}
|
||||
}
|
||||
|
||||
override fun isBiometricUnlockAllowed(): Flow<Boolean> {
|
||||
return context.dataStore.data.map { preferences ->
|
||||
return dataStore.data.map { preferences ->
|
||||
preferences[biometricUnlockKey] ?: false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
|
||||
context.dataStore.edit { preferences ->
|
||||
dataStore.edit { preferences ->
|
||||
preferences[biometricUnlockKey] = isAllowed
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ setupAnvil()
|
|||
dependencies {
|
||||
implementation(projects.features.migration.api)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.androidutils)
|
||||
implementation(projects.libraries.preferences.impl)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
implementation(projects.features.rageshake.api)
|
||||
|
|
|
|||
|
|
@ -7,27 +7,22 @@
|
|||
|
||||
package io.element.android.features.migration.impl
|
||||
|
||||
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.intPreferencesKey
|
||||
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.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_migration")
|
||||
private val applicationMigrationVersion = intPreferencesKey("applicationMigrationVersion")
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultMigrationStore @Inject constructor(
|
||||
@ApplicationContext context: Context,
|
||||
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||
) : MigrationStore {
|
||||
private val store = context.dataStore
|
||||
private val store = preferenceDataStoreFactory.create("elementx_migration")
|
||||
|
||||
override suspend fun setApplicationMigrationVersion(version: Int) {
|
||||
store.edit { prefs ->
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ dependencies {
|
|||
implementation(projects.libraries.network)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.designsystem)
|
||||
implementation(projects.libraries.preferences.api)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
|
@ -56,6 +57,7 @@ dependencies {
|
|||
testImplementation(projects.libraries.sessionStorage.implMemory)
|
||||
testImplementation(projects.libraries.sessionStorage.test)
|
||||
testImplementation(projects.features.rageshake.test)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
testImplementation(projects.services.toolbox.test)
|
||||
testImplementation(libs.network.mockwebserver)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue