Skip to content

Handle preference stores corruption by clearing them #5086

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions features/lockscreen/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,88 +7,85 @@

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
}
}

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
}
}
Expand Down
1 change: 1 addition & 0 deletions features/migration/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,24 @@

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.di.SingleIn
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")

@SingleIn(AppScope::class)
@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 ->
Expand Down
1 change: 1 addition & 0 deletions features/rageshake/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,28 @@

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.di.SingleIn
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")

@SingleIn(AppScope::class)
@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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ package io.element.android.features.rageshake.impl.crash

import android.content.Context
import android.os.Build
import io.element.android.features.rageshake.impl.di.RageshakeBindings
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.data.tryOrNull
import timber.log.Timber
import java.io.PrintWriter
Expand All @@ -17,7 +19,7 @@ import java.io.StringWriter
class VectorUncaughtExceptionHandler(
context: Context
) : Thread.UncaughtExceptionHandler {
private val crashDataStore = PreferencesCrashDataStore(context)
private val crashDataStore = context.bindings<RageshakeBindings>().preferencesCrashDataStore()
private var previousHandler: Thread.UncaughtExceptionHandler? = null

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
3 changes: 3 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ com.squareup.anvil.kspContributingAnnotations=io.element.android.anvilannotation

# Only apply KSP to main sources
ksp.allow.all.target.configuration=false

# Used to prevent detekt from reusing invalid cached rules
detekt.use.worker.api=true
1 change: 1 addition & 0 deletions libraries/androidutils/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies {
implementation(libs.androidx.activity.activity)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.exifinterface)
implementation(libs.androidx.datastore.preferences)
api(libs.androidx.browser)

testImplementation(projects.tests.testutils)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.libraries.androidutils.preferences

import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences

object DefaultPreferencesCorruptionHandlerFactory {
/**
* Creates a [ReplaceFileCorruptionHandler] that will replace the corrupted preferences file with an empty preferences object.
*/
fun replaceWithEmpty(): ReplaceFileCorruptionHandler<Preferences> {
return ReplaceFileCorruptionHandler(
produceNewData = {
// If the preferences file is corrupted, we return an empty preferences object
emptyPreferences()
},
)
}
}
2 changes: 2 additions & 0 deletions libraries/featureflag/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ dependencies {
implementation(libs.androidx.datastore.preferences)
implementation(projects.appconfig)
implementation(projects.libraries.di)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.preferences.api)
implementation(libs.coroutines.core)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,24 @@

package io.element.android.libraries.featureflag.impl

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.preferencesDataStore
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import javax.inject.Inject

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_featureflag")

/**
* Note: this will be used only in the nightly and in the debug build.
*/
class PreferencesFeatureFlagProvider @Inject constructor(
@ApplicationContext context: Context,
private val buildMeta: BuildMeta,
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : MutableFeatureFlagProvider {
private val store = context.dataStore
private val store = preferenceDataStoreFactory.create("elementx_featureflag")

override val priority = MEDIUM_PRIORITY

Expand Down
1 change: 1 addition & 0 deletions libraries/permissions/impl/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
implementation(projects.libraries.troubleshoot.api)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
implementation(projects.libraries.preferences.api)
implementation(projects.services.toolbox.api)
api(projects.libraries.permissions.api)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,24 @@

package io.element.android.libraries.permissions.impl

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.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.di.SingleIn
import io.element.android.libraries.permissions.api.PermissionsStore
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 = "permissions_store")

@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultPermissionsStore @Inject constructor(
@ApplicationContext private val context: Context,
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : PermissionsStore {
private val store = context.dataStore
private val store = preferenceDataStoreFactory.create("permissions_store")

override suspend fun setPermissionDenied(permission: String, value: Boolean) {
store.edit { prefs ->
Expand Down
Loading
Loading