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
b54b74a198
commit
2f2e886e9f
30 changed files with 198 additions and 138 deletions
|
|
@ -10,10 +10,14 @@ package io.element.android.x.initializer
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.startup.Initializer
|
import androidx.startup.Initializer
|
||||||
import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler
|
import io.element.android.features.rageshake.impl.crash.VectorUncaughtExceptionHandler
|
||||||
|
import io.element.android.features.rageshake.impl.di.RageshakeBindings
|
||||||
|
import io.element.android.libraries.architecture.bindings
|
||||||
|
|
||||||
class CrashInitializer : Initializer<Unit> {
|
class CrashInitializer : Initializer<Unit> {
|
||||||
override fun create(context: Context) {
|
override fun create(context: Context) {
|
||||||
VectorUncaughtExceptionHandler(context).activate()
|
VectorUncaughtExceptionHandler(
|
||||||
|
context.bindings<RageshakeBindings>().preferencesCrashDataStore(),
|
||||||
|
).activate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ dependencies {
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
implementation(projects.features.enterprise.api)
|
implementation(projects.features.enterprise.api)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
|
|
|
||||||
|
|
@ -7,44 +7,39 @@
|
||||||
|
|
||||||
package io.element.android.features.lockscreen.impl.storage
|
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.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.lockscreen.impl.LockScreenConfig
|
import io.element.android.features.lockscreen.impl.LockScreenConfig
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||||
import io.element.android.libraries.di.SingleIn
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "pin_code_store")
|
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class PreferencesLockScreenStore @Inject constructor(
|
class PreferencesLockScreenStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
private val lockScreenConfig: LockScreenConfig,
|
private val lockScreenConfig: LockScreenConfig,
|
||||||
) : LockScreenStore {
|
) : LockScreenStore {
|
||||||
|
private val dataStore = preferenceDataStoreFactory.create("pin_code_store")
|
||||||
|
|
||||||
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
|
private val pinCodeKey = stringPreferencesKey("encoded_pin_code")
|
||||||
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
|
private val remainingAttemptsKey = intPreferencesKey("remaining_pin_code_attempts")
|
||||||
private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled")
|
private val biometricUnlockKey = booleanPreferencesKey("biometric_unlock_enabled")
|
||||||
|
|
||||||
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
override suspend fun getRemainingPinCodeAttemptsNumber(): Int {
|
||||||
return context.dataStore.data.map { preferences ->
|
return dataStore.data.map { preferences ->
|
||||||
preferences.getRemainingPinCodeAttemptsNumber()
|
preferences.getRemainingPinCodeAttemptsNumber()
|
||||||
}.first()
|
}.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun onWrongPin() {
|
override suspend fun onWrongPin() {
|
||||||
context.dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
val current = preferences.getRemainingPinCodeAttemptsNumber()
|
val current = preferences.getRemainingPinCodeAttemptsNumber()
|
||||||
val remaining = (current - 1).coerceAtLeast(0)
|
val remaining = (current - 1).coerceAtLeast(0)
|
||||||
preferences[remainingAttemptsKey] = remaining
|
preferences[remainingAttemptsKey] = remaining
|
||||||
|
|
@ -52,43 +47,43 @@ class PreferencesLockScreenStore @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun resetCounter() {
|
override suspend fun resetCounter() {
|
||||||
context.dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout
|
preferences[remainingAttemptsKey] = lockScreenConfig.maxPinCodeAttemptsBeforeLogout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getEncryptedCode(): String? {
|
override suspend fun getEncryptedCode(): String? {
|
||||||
return context.dataStore.data.map { preferences ->
|
return dataStore.data.map { preferences ->
|
||||||
preferences[pinCodeKey]
|
preferences[pinCodeKey]
|
||||||
}.first()
|
}.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveEncryptedPinCode(pinCode: String) {
|
override suspend fun saveEncryptedPinCode(pinCode: String) {
|
||||||
context.dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
preferences[pinCodeKey] = pinCode
|
preferences[pinCodeKey] = pinCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteEncryptedPinCode() {
|
override suspend fun deleteEncryptedPinCode() {
|
||||||
context.dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
preferences.remove(pinCodeKey)
|
preferences.remove(pinCodeKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hasPinCode(): Flow<Boolean> {
|
override fun hasPinCode(): Flow<Boolean> {
|
||||||
return context.dataStore.data.map { preferences ->
|
return dataStore.data.map { preferences ->
|
||||||
preferences[pinCodeKey] != null
|
preferences[pinCodeKey] != null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isBiometricUnlockAllowed(): Flow<Boolean> {
|
override fun isBiometricUnlockAllowed(): Flow<Boolean> {
|
||||||
return context.dataStore.data.map { preferences ->
|
return dataStore.data.map { preferences ->
|
||||||
preferences[biometricUnlockKey] ?: false
|
preferences[biometricUnlockKey] ?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
|
override suspend fun setIsBiometricUnlockAllowed(isAllowed: Boolean) {
|
||||||
context.dataStore.edit { preferences ->
|
dataStore.edit { preferences ->
|
||||||
preferences[biometricUnlockKey] = isAllowed
|
preferences[biometricUnlockKey] = isAllowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ setupAnvil()
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.features.migration.api)
|
implementation(projects.features.migration.api)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.preferences.impl)
|
implementation(projects.libraries.preferences.impl)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(projects.features.rageshake.api)
|
implementation(projects.features.rageshake.api)
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,22 @@
|
||||||
|
|
||||||
package io.element.android.features.migration.impl
|
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.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_migration")
|
|
||||||
private val applicationMigrationVersion = intPreferencesKey("applicationMigrationVersion")
|
private val applicationMigrationVersion = intPreferencesKey("applicationMigrationVersion")
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultMigrationStore @Inject constructor(
|
class DefaultMigrationStore @Inject constructor(
|
||||||
@ApplicationContext context: Context,
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : MigrationStore {
|
) : MigrationStore {
|
||||||
private val store = context.dataStore
|
private val store = preferenceDataStoreFactory.create("elementx_migration")
|
||||||
|
|
||||||
override suspend fun setApplicationMigrationVersion(version: Int) {
|
override suspend fun setApplicationMigrationVersion(version: Int) {
|
||||||
store.edit { prefs ->
|
store.edit { prefs ->
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
implementation(projects.libraries.network)
|
implementation(projects.libraries.network)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.sessionStorage.api)
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
|
@ -56,6 +57,7 @@ dependencies {
|
||||||
testImplementation(projects.libraries.sessionStorage.implMemory)
|
testImplementation(projects.libraries.sessionStorage.implMemory)
|
||||||
testImplementation(projects.libraries.sessionStorage.test)
|
testImplementation(projects.libraries.sessionStorage.test)
|
||||||
testImplementation(projects.features.rageshake.test)
|
testImplementation(projects.features.rageshake.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(projects.services.toolbox.test)
|
testImplementation(projects.services.toolbox.test)
|
||||||
testImplementation(libs.network.mockwebserver)
|
testImplementation(libs.network.mockwebserver)
|
||||||
|
|
|
||||||
|
|
@ -7,32 +7,26 @@
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.crash
|
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.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.AppScope
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_crash")
|
|
||||||
|
|
||||||
private val appHasCrashedKey = booleanPreferencesKey("appHasCrashed")
|
private val appHasCrashedKey = booleanPreferencesKey("appHasCrashed")
|
||||||
private val crashDataKey = stringPreferencesKey("crashData")
|
private val crashDataKey = stringPreferencesKey("crashData")
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class PreferencesCrashDataStore @Inject constructor(
|
class PreferencesCrashDataStore @Inject constructor(
|
||||||
@ApplicationContext context: Context
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : CrashDataStore {
|
) : CrashDataStore {
|
||||||
private val store = context.dataStore
|
private val store = preferenceDataStoreFactory.create("elementx_crash")
|
||||||
|
|
||||||
override fun setCrashData(crashData: String) {
|
override fun setCrashData(crashData: String) {
|
||||||
// Must block
|
// Must block
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
|
|
||||||
package io.element.android.features.rageshake.impl.crash
|
package io.element.android.features.rageshake.impl.crash
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
@ -15,9 +14,8 @@ import java.io.PrintWriter
|
||||||
import java.io.StringWriter
|
import java.io.StringWriter
|
||||||
|
|
||||||
class VectorUncaughtExceptionHandler(
|
class VectorUncaughtExceptionHandler(
|
||||||
context: Context
|
private val preferencesCrashDataStore: PreferencesCrashDataStore,
|
||||||
) : Thread.UncaughtExceptionHandler {
|
) : Thread.UncaughtExceptionHandler {
|
||||||
private val crashDataStore = PreferencesCrashDataStore(context)
|
|
||||||
private var previousHandler: Thread.UncaughtExceptionHandler? = null
|
private var previousHandler: Thread.UncaughtExceptionHandler? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -65,7 +63,7 @@ class VectorUncaughtExceptionHandler(
|
||||||
append(sw.buffer.toString())
|
append(sw.buffer.toString())
|
||||||
}
|
}
|
||||||
Timber.e("FATAL EXCEPTION $bugDescription")
|
Timber.e("FATAL EXCEPTION $bugDescription")
|
||||||
crashDataStore.setCrashData(bugDescription)
|
preferencesCrashDataStore.setCrashData(bugDescription)
|
||||||
// Show the classical system popup
|
// Show the classical system popup
|
||||||
previousHandler?.uncaughtException(thread, throwable)
|
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
|
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.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.floatPreferencesKey
|
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.AppScope
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_rageshake")
|
|
||||||
|
|
||||||
private val enabledKey = booleanPreferencesKey("enabled")
|
private val enabledKey = booleanPreferencesKey("enabled")
|
||||||
private val sensitivityKey = floatPreferencesKey("sensitivity")
|
private val sensitivityKey = floatPreferencesKey("sensitivity")
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class PreferencesRageshakeDataStore @Inject constructor(
|
class PreferencesRageshakeDataStore @Inject constructor(
|
||||||
@ApplicationContext context: Context
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : RageshakeDataStore {
|
) : RageshakeDataStore {
|
||||||
private val store = context.dataStore
|
private val store = preferenceDataStoreFactory.create("elementx_rageshake")
|
||||||
|
|
||||||
override fun isEnabled(): Flow<Boolean> {
|
override fun isEnabled(): Flow<Boolean> {
|
||||||
return store.data.map { prefs ->
|
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 com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
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.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.RuntimeEnvironment
|
|
||||||
|
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class VectorUncaughtExceptionHandlerTest {
|
class VectorUncaughtExceptionHandlerTest {
|
||||||
@Test
|
@Test
|
||||||
fun `activate should change the default handler`() {
|
fun `activate should change the default handler`() {
|
||||||
val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
|
val sut = VectorUncaughtExceptionHandler(PreferencesCrashDataStore(FakePreferenceDataStoreFactory()))
|
||||||
sut.activate()
|
sut.activate()
|
||||||
assertThat(Thread.getDefaultUncaughtExceptionHandler()).isInstanceOf(VectorUncaughtExceptionHandler::class.java)
|
assertThat(Thread.getDefaultUncaughtExceptionHandler()).isInstanceOf(VectorUncaughtExceptionHandler::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `uncaught exception`() = runTest {
|
fun `uncaught exception`() = runTest {
|
||||||
val crashDataStore = PreferencesCrashDataStore(RuntimeEnvironment.getApplication())
|
val crashDataStore = PreferencesCrashDataStore(FakePreferenceDataStoreFactory())
|
||||||
assertThat(crashDataStore.appHasCrashed().first()).isFalse()
|
assertThat(crashDataStore.appHasCrashed().first()).isFalse()
|
||||||
assertThat(crashDataStore.crashInfo().first()).isEmpty()
|
assertThat(crashDataStore.crashInfo().first()).isEmpty()
|
||||||
val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
|
val sut = VectorUncaughtExceptionHandler(crashDataStore)
|
||||||
sut.uncaughtException(Thread(), AN_EXCEPTION)
|
sut.uncaughtException(Thread(), AN_EXCEPTION)
|
||||||
assertThat(crashDataStore.appHasCrashed().first()).isTrue()
|
assertThat(crashDataStore.appHasCrashed().first()).isTrue()
|
||||||
val crashInfo = crashDataStore.crashInfo().first()
|
val crashInfo = crashDataStore.crashInfo().first()
|
||||||
|
|
|
||||||
|
|
@ -54,3 +54,6 @@ com.squareup.anvil.kspContributingAnnotations=io.element.android.anvilannotation
|
||||||
|
|
||||||
# Only apply KSP to main sources
|
# Only apply KSP to main sources
|
||||||
ksp.allow.all.target.configuration=false
|
ksp.allow.all.target.configuration=false
|
||||||
|
|
||||||
|
# Used to prevent detekt from reusing invalid cached rules
|
||||||
|
detekt.use.worker.api=true
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ dependencies {
|
||||||
implementation(libs.androidx.activity.activity)
|
implementation(libs.androidx.activity.activity)
|
||||||
implementation(libs.androidx.recyclerview)
|
implementation(libs.androidx.recyclerview)
|
||||||
implementation(libs.androidx.exifinterface)
|
implementation(libs.androidx.exifinterface)
|
||||||
|
implementation(libs.androidx.datastore.preferences)
|
||||||
api(libs.androidx.browser)
|
api(libs.androidx.browser)
|
||||||
|
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,7 +24,9 @@ dependencies {
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
implementation(projects.appconfig)
|
implementation(projects.appconfig)
|
||||||
implementation(projects.libraries.di)
|
implementation(projects.libraries.di)
|
||||||
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -7,30 +7,24 @@
|
||||||
|
|
||||||
package io.element.android.libraries.featureflag.impl
|
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.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
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.featureflag.api.Feature
|
||||||
|
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
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.
|
* Note: this will be used only in the nightly and in the debug build.
|
||||||
*/
|
*/
|
||||||
class PreferencesFeatureFlagProvider @Inject constructor(
|
class PreferencesFeatureFlagProvider @Inject constructor(
|
||||||
@ApplicationContext context: Context,
|
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : MutableFeatureFlagProvider {
|
) : MutableFeatureFlagProvider {
|
||||||
private val store = context.dataStore
|
private val store = preferenceDataStoreFactory.create("elementx_featureflag")
|
||||||
|
|
||||||
override val priority = MEDIUM_PRIORITY
|
override val priority = MEDIUM_PRIORITY
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ dependencies {
|
||||||
implementation(projects.libraries.troubleshoot.api)
|
implementation(projects.libraries.troubleshoot.api)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.services.toolbox.api)
|
implementation(projects.services.toolbox.api)
|
||||||
api(projects.libraries.permissions.api)
|
api(projects.libraries.permissions.api)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,22 @@
|
||||||
|
|
||||||
package io.element.android.libraries.permissions.impl
|
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.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
|
||||||
import io.element.android.libraries.permissions.api.PermissionsStore
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "permissions_store")
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultPermissionsStore @Inject constructor(
|
class DefaultPermissionsStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : PermissionsStore {
|
) : PermissionsStore {
|
||||||
private val store = context.dataStore
|
private val store = preferenceDataStoreFactory.create("permissions_store")
|
||||||
|
|
||||||
override suspend fun setPermissionDenied(permission: String, value: Boolean) {
|
override suspend fun setPermissionDenied(permission: String, value: Boolean) {
|
||||||
store.edit { prefs ->
|
store.edit { prefs ->
|
||||||
|
|
|
||||||
|
|
@ -7,28 +7,22 @@
|
||||||
|
|
||||||
package io.element.android.libraries.preferences.impl.store
|
package io.element.android.libraries.preferences.impl.store
|
||||||
|
|
||||||
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.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
|
||||||
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
import io.element.android.libraries.matrix.api.media.MediaPreviewValue
|
||||||
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
import io.element.android.libraries.matrix.api.tracing.LogLevel
|
||||||
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
import io.element.android.libraries.matrix.api.tracing.TraceLogPack
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
|
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_preferences")
|
|
||||||
|
|
||||||
private val developerModeKey = booleanPreferencesKey("developerMode")
|
private val developerModeKey = booleanPreferencesKey("developerMode")
|
||||||
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
|
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
|
||||||
private val themeKey = stringPreferencesKey("theme")
|
private val themeKey = stringPreferencesKey("theme")
|
||||||
|
|
@ -39,10 +33,10 @@ private val traceLogPacksKey = stringPreferencesKey("traceLogPacks")
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultAppPreferencesStore @Inject constructor(
|
class DefaultAppPreferencesStore @Inject constructor(
|
||||||
@ApplicationContext context: Context,
|
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : AppPreferencesStore {
|
) : AppPreferencesStore {
|
||||||
private val store = context.dataStore
|
private val store = preferenceDataStoreFactory.create("elementx_preferences")
|
||||||
|
|
||||||
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
|
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
|
||||||
store.edit { prefs ->
|
store.edit { prefs ->
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.datastore.core.DataStore
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
import androidx.datastore.preferences.preferencesDataStore
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.androidutils.preferences.DefaultPreferencesCorruptionHandlerFactory
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
|
|
@ -27,7 +28,10 @@ class DefaultPreferencesDataStoreFactory @Inject constructor(
|
||||||
private val dataStoreHolders = ConcurrentHashMap<String, DataStoreHolder>()
|
private val dataStoreHolders = ConcurrentHashMap<String, DataStoreHolder>()
|
||||||
|
|
||||||
private class DataStoreHolder(name: String) {
|
private class DataStoreHolder(name: String) {
|
||||||
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = name)
|
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
|
||||||
|
name = name,
|
||||||
|
corruptionHandler = DefaultPreferencesCorruptionHandlerFactory.replaceWithEmpty(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun create(name: String): DataStore<Preferences> {
|
override fun create(name: String): DataStore<Preferences> {
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,8 @@
|
||||||
|
|
||||||
package io.element.android.libraries.push.impl.store
|
package io.element.android.libraries.push.impl.store
|
||||||
|
|
||||||
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.edit
|
||||||
import androidx.datastore.preferences.core.intPreferencesKey
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import app.cash.sqldelight.coroutines.asFlow
|
import app.cash.sqldelight.coroutines.asFlow
|
||||||
import app.cash.sqldelight.coroutines.mapToList
|
import app.cash.sqldelight.coroutines.mapToList
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
|
@ -20,29 +16,30 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.dateformatter.api.DateFormatter
|
import io.element.android.libraries.dateformatter.api.DateFormatter
|
||||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.di.AppScope
|
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.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||||
import io.element.android.libraries.push.api.history.PushHistoryItem
|
import io.element.android.libraries.push.api.history.PushHistoryItem
|
||||||
import io.element.android.libraries.push.impl.PushDatabase
|
import io.element.android.libraries.push.impl.PushDatabase
|
||||||
|
import io.element.android.libraries.push.impl.store.DefaultPushDataStore.Companion.BATTERY_OPTIMIZATION_BANNER_STATE_DISMISSED
|
||||||
|
import io.element.android.libraries.push.impl.store.DefaultPushDataStore.Companion.BATTERY_OPTIMIZATION_BANNER_STATE_INIT
|
||||||
|
import io.element.android.libraries.push.impl.store.DefaultPushDataStore.Companion.BATTERY_OPTIMIZATION_BANNER_STATE_SHOW
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_store")
|
|
||||||
|
|
||||||
@SingleIn(AppScope::class)
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultPushDataStore @Inject constructor(
|
class DefaultPushDataStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val pushDatabase: PushDatabase,
|
private val pushDatabase: PushDatabase,
|
||||||
private val dateFormatter: DateFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
|
preferencesFactory: PreferenceDataStoreFactory,
|
||||||
) : PushDataStore {
|
) : PushDataStore {
|
||||||
private val pushCounter = intPreferencesKey("push_counter")
|
private val pushCounter = intPreferencesKey("push_counter")
|
||||||
|
|
||||||
|
private val dataStore = preferencesFactory.create("push_store")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Integer preference to track the state of the battery optimization banner.
|
* Integer preference to track the state of the battery optimization banner.
|
||||||
* Possible values:
|
* Possible values:
|
||||||
|
|
@ -52,24 +49,24 @@ class DefaultPushDataStore @Inject constructor(
|
||||||
*/
|
*/
|
||||||
private val batteryOptimizationBannerState = intPreferencesKey("battery_optimization_banner_state")
|
private val batteryOptimizationBannerState = intPreferencesKey("battery_optimization_banner_state")
|
||||||
|
|
||||||
override val pushCounterFlow: Flow<Int> = context.dataStore.data.map { preferences ->
|
override val pushCounterFlow: Flow<Int> = dataStore.data.map { preferences ->
|
||||||
preferences[pushCounter] ?: 0
|
preferences[pushCounter] ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UnnecessaryParentheses")
|
@Suppress("UnnecessaryParentheses")
|
||||||
override val shouldDisplayBatteryOptimizationBannerFlow: Flow<Boolean> = context.dataStore.data.map { preferences ->
|
override val shouldDisplayBatteryOptimizationBannerFlow: Flow<Boolean> = dataStore.data.map { preferences ->
|
||||||
(preferences[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT) == BATTERY_OPTIMIZATION_BANNER_STATE_SHOW
|
(preferences[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT) == BATTERY_OPTIMIZATION_BANNER_STATE_SHOW
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun incrementPushCounter() {
|
suspend fun incrementPushCounter() {
|
||||||
context.dataStore.edit { settings ->
|
dataStore.edit { settings ->
|
||||||
val currentCounterValue = settings[pushCounter] ?: 0
|
val currentCounterValue = settings[pushCounter] ?: 0
|
||||||
settings[pushCounter] = currentCounterValue + 1
|
settings[pushCounter] = currentCounterValue + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setBatteryOptimizationBannerState(newState: Int) {
|
suspend fun setBatteryOptimizationBannerState(newState: Int) {
|
||||||
context.dataStore.edit { settings ->
|
dataStore.edit { settings ->
|
||||||
val currentValue = settings[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT
|
val currentValue = settings[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT
|
||||||
settings[batteryOptimizationBannerState] = when (currentValue) {
|
settings[batteryOptimizationBannerState] = when (currentValue) {
|
||||||
BATTERY_OPTIMIZATION_BANNER_STATE_INIT,
|
BATTERY_OPTIMIZATION_BANNER_STATE_INIT,
|
||||||
|
|
@ -106,7 +103,7 @@ class DefaultPushDataStore @Inject constructor(
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
pushDatabase.pushHistoryQueries.removeAll()
|
pushDatabase.pushHistoryQueries.removeAll()
|
||||||
context.dataStore.edit {
|
dataStore.edit {
|
||||||
it.clear()
|
it.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ dependencies {
|
||||||
implementation(projects.libraries.androidutils)
|
implementation(projects.libraries.androidutils)
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.pushstore.api)
|
implementation(projects.libraries.pushstore.api)
|
||||||
implementation(libs.androidx.corektx)
|
implementation(libs.androidx.corektx)
|
||||||
implementation(libs.androidx.datastore.preferences)
|
implementation(libs.androidx.datastore.preferences)
|
||||||
|
|
@ -38,6 +39,7 @@ dependencies {
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.services.appnavstate.test)
|
testImplementation(projects.services.appnavstate.test)
|
||||||
testImplementation(projects.libraries.pushstore.test)
|
testImplementation(projects.libraries.pushstore.test)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import io.element.android.libraries.di.AppScope
|
||||||
import io.element.android.libraries.di.ApplicationContext
|
import io.element.android.libraries.di.ApplicationContext
|
||||||
import io.element.android.libraries.di.SingleIn
|
import io.element.android.libraries.di.SingleIn
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
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.UserPushStore
|
||||||
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
import io.element.android.libraries.pushstore.api.UserPushStoreFactory
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
@ -22,6 +23,7 @@ import javax.inject.Inject
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultUserPushStoreFactory @Inject constructor(
|
class DefaultUserPushStoreFactory @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
|
private val preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : UserPushStoreFactory {
|
) : UserPushStoreFactory {
|
||||||
// We can have only one class accessing a single data store, so keep a cache of them.
|
// We can have only one class accessing a single data store, so keep a cache of them.
|
||||||
private val cache = ConcurrentHashMap<SessionId, UserPushStore>()
|
private val cache = ConcurrentHashMap<SessionId, UserPushStore>()
|
||||||
|
|
@ -29,7 +31,8 @@ class DefaultUserPushStoreFactory @Inject constructor(
|
||||||
return cache.getOrPut(userId) {
|
return cache.getOrPut(userId) {
|
||||||
UserPushStoreDataStore(
|
UserPushStoreDataStore(
|
||||||
context = context,
|
context = context,
|
||||||
userId = userId
|
userId = userId,
|
||||||
|
factory = preferenceDataStoreFactory,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ import androidx.datastore.preferences.core.Preferences
|
||||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import androidx.datastore.preferences.preferencesDataStoreFile
|
import androidx.datastore.preferences.preferencesDataStoreFile
|
||||||
import io.element.android.libraries.androidutils.hash.hash
|
import io.element.android.libraries.androidutils.hash.hash
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.core.bool.orTrue
|
import io.element.android.libraries.core.bool.orTrue
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
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.UserPushStore
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
@ -31,6 +31,7 @@ import timber.log.Timber
|
||||||
class UserPushStoreDataStore(
|
class UserPushStoreDataStore(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
userId: SessionId,
|
userId: SessionId,
|
||||||
|
factory: PreferenceDataStoreFactory,
|
||||||
) : UserPushStore {
|
) : UserPushStore {
|
||||||
// Hash the sessionId to get rid of exotic chars and take only the first 16 chars.
|
// Hash the sessionId to get rid of exotic chars and take only the first 16 chars.
|
||||||
// The risk of collision is not high.
|
// 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 pushProviderName = stringPreferencesKey("pushProviderName")
|
||||||
private val currentPushKey = stringPreferencesKey("currentPushKey")
|
private val currentPushKey = stringPreferencesKey("currentPushKey")
|
||||||
private val notificationEnabled = booleanPreferencesKey("notificationEnabled")
|
private val notificationEnabled = booleanPreferencesKey("notificationEnabled")
|
||||||
private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError")
|
private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError")
|
||||||
|
|
||||||
override suspend fun getPushProviderName(): String? {
|
override suspend fun getPushProviderName(): String? {
|
||||||
return context.dataStore.data.first()[pushProviderName]
|
return store.data.first()[pushProviderName]
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setPushProviderName(value: String) {
|
override suspend fun setPushProviderName(value: String) {
|
||||||
context.dataStore.edit {
|
store.edit {
|
||||||
it[pushProviderName] = value
|
it[pushProviderName] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getCurrentRegisteredPushKey(): String? {
|
override suspend fun getCurrentRegisteredPushKey(): String? {
|
||||||
return context.dataStore.data.first()[currentPushKey]
|
return store.data.first()[currentPushKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setCurrentRegisteredPushKey(value: String?) {
|
override suspend fun setCurrentRegisteredPushKey(value: String?) {
|
||||||
context.dataStore.edit {
|
store.edit {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
it.remove(currentPushKey)
|
it.remove(currentPushKey)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -80,11 +81,11 @@ class UserPushStoreDataStore(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getNotificationEnabledForDevice(): Flow<Boolean> {
|
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) {
|
override suspend fun setNotificationEnabledForDevice(enabled: Boolean) {
|
||||||
context.dataStore.edit {
|
store.edit {
|
||||||
it[notificationEnabled] = enabled
|
it[notificationEnabled] = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -94,17 +95,17 @@ class UserPushStoreDataStore(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun ignoreRegistrationError(): Flow<Boolean> {
|
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) {
|
override suspend fun setIgnoreRegistrationError(ignore: Boolean) {
|
||||||
context.dataStore.edit {
|
store.edit {
|
||||||
it[ignoreRegistrationError] = ignore
|
it[ignoreRegistrationError] = ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
context.dataStore.edit {
|
store.edit {
|
||||||
it.clear()
|
it.clear()
|
||||||
}
|
}
|
||||||
// Also delete the file
|
// Also delete the file
|
||||||
|
|
|
||||||
|
|
@ -7,44 +7,40 @@
|
||||||
|
|
||||||
package io.element.android.libraries.pushstore.impl.clientsecret
|
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.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.di.AppScope
|
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.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "push_client_secret_store")
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DataStorePushClientSecretStore @Inject constructor(
|
class DataStorePushClientSecretStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : PushClientSecretStore {
|
) : PushClientSecretStore {
|
||||||
|
private val dataStore = preferenceDataStoreFactory.create("push_client_secret_store")
|
||||||
|
|
||||||
override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
|
override suspend fun storeSecret(userId: SessionId, clientSecret: String) {
|
||||||
context.dataStore.edit { settings ->
|
dataStore.edit { settings ->
|
||||||
settings[getPreferenceKeyForUser(userId)] = clientSecret
|
settings[getPreferenceKeyForUser(userId)] = clientSecret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSecret(userId: SessionId): String? {
|
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) {
|
override suspend fun resetSecret(userId: SessionId) {
|
||||||
context.dataStore.edit { settings ->
|
dataStore.edit { settings ->
|
||||||
settings.remove(getPreferenceKeyForUser(userId))
|
settings.remove(getPreferenceKeyForUser(userId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? {
|
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 {
|
val matchingKey = keyValues.keys.find {
|
||||||
keyValues[it] == clientSecret
|
keyValues[it] == clientSecret
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
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.matrix.test.A_SESSION_ID_2
|
||||||
|
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -92,5 +93,6 @@ class UserPushStoreDataStoreTest {
|
||||||
) = UserPushStoreDataStore(
|
) = UserPushStoreDataStore(
|
||||||
context = InstrumentationRegistry.getInstrumentation().context,
|
context = InstrumentationRegistry.getInstrumentation().context,
|
||||||
userId = sessionId,
|
userId = sessionId,
|
||||||
|
factory = FakePreferenceDataStoreFactory(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.sessionStorage.api)
|
implementation(projects.libraries.sessionStorage.api)
|
||||||
|
|
||||||
api(projects.services.analyticsproviders.api)
|
api(projects.services.analyticsproviders.api)
|
||||||
|
|
|
||||||
|
|
@ -7,27 +7,18 @@
|
||||||
|
|
||||||
package io.element.android.services.analytics.impl.store
|
package io.element.android.services.analytics.impl.store
|
||||||
|
|
||||||
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.booleanPreferencesKey
|
||||||
import androidx.datastore.preferences.core.edit
|
import androidx.datastore.preferences.core.edit
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
import io.element.android.libraries.di.AppScope
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
|
||||||
* Also accessed via reflection by the instrumentation tests @see [im.vector.app.ClearCurrentSessionRule].
|
|
||||||
*/
|
|
||||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "vector_analytics")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Local storage for:
|
* Local storage for:
|
||||||
* - user consent (Boolean);
|
* - user consent (Boolean);
|
||||||
|
|
@ -46,44 +37,46 @@ interface AnalyticsStore {
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultAnalyticsStore @Inject constructor(
|
class DefaultAnalyticsStore @Inject constructor(
|
||||||
@ApplicationContext private val context: Context
|
preferenceDataStoreFactory: PreferenceDataStoreFactory,
|
||||||
) : AnalyticsStore {
|
) : AnalyticsStore {
|
||||||
private val userConsent = booleanPreferencesKey("user_consent")
|
private val userConsent = booleanPreferencesKey("user_consent")
|
||||||
private val didAskUserConsent = booleanPreferencesKey("did_ask_user_consent")
|
private val didAskUserConsent = booleanPreferencesKey("did_ask_user_consent")
|
||||||
private val analyticsId = stringPreferencesKey("analytics_id")
|
private val analyticsId = stringPreferencesKey("analytics_id")
|
||||||
|
|
||||||
override val userConsentFlow: Flow<Boolean> = context.dataStore.data
|
private val dataStore = preferenceDataStoreFactory.create("vector_analytics")
|
||||||
|
|
||||||
|
override val userConsentFlow: Flow<Boolean> = dataStore.data
|
||||||
.map { preferences -> preferences[userConsent].orFalse() }
|
.map { preferences -> preferences[userConsent].orFalse() }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
||||||
override val didAskUserConsentFlow: Flow<Boolean> = context.dataStore.data
|
override val didAskUserConsentFlow: Flow<Boolean> = dataStore.data
|
||||||
.map { preferences -> preferences[didAskUserConsent].orFalse() }
|
.map { preferences -> preferences[didAskUserConsent].orFalse() }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
||||||
override val analyticsIdFlow: Flow<String> = context.dataStore.data
|
override val analyticsIdFlow: Flow<String> = dataStore.data
|
||||||
.map { preferences -> preferences[analyticsId].orEmpty() }
|
.map { preferences -> preferences[analyticsId].orEmpty() }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
||||||
override suspend fun setUserConsent(newUserConsent: Boolean) {
|
override suspend fun setUserConsent(newUserConsent: Boolean) {
|
||||||
context.dataStore.edit { settings ->
|
dataStore.edit { settings ->
|
||||||
settings[userConsent] = newUserConsent
|
settings[userConsent] = newUserConsent
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setDidAskUserConsent(newValue: Boolean) {
|
override suspend fun setDidAskUserConsent(newValue: Boolean) {
|
||||||
context.dataStore.edit { settings ->
|
dataStore.edit { settings ->
|
||||||
settings[didAskUserConsent] = newValue
|
settings[didAskUserConsent] = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun setAnalyticsId(newAnalyticsId: String) {
|
override suspend fun setAnalyticsId(newAnalyticsId: String) {
|
||||||
context.dataStore.edit { settings ->
|
dataStore.edit { settings ->
|
||||||
settings[analyticsId] = newAnalyticsId
|
settings[analyticsId] = newAnalyticsId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
context.dataStore.edit {
|
dataStore.edit {
|
||||||
it.clear()
|
it.clear()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* 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.detektrules
|
||||||
|
|
||||||
|
import io.github.detekt.psi.fileName
|
||||||
|
import io.gitlab.arturbosch.detekt.api.CodeSmell
|
||||||
|
import io.gitlab.arturbosch.detekt.api.Config
|
||||||
|
import io.gitlab.arturbosch.detekt.api.Debt
|
||||||
|
import io.gitlab.arturbosch.detekt.api.Entity
|
||||||
|
import io.gitlab.arturbosch.detekt.api.Issue
|
||||||
|
import io.gitlab.arturbosch.detekt.api.Rule
|
||||||
|
import io.gitlab.arturbosch.detekt.api.Severity
|
||||||
|
import org.jetbrains.kotlin.psi.KtPropertyDelegate
|
||||||
|
|
||||||
|
class ByPreferencesDataStoreRule(config: Config) : Rule(config) {
|
||||||
|
override val issue: Issue = Issue(
|
||||||
|
id = "ByPreferencesDataStoreNotAllowed",
|
||||||
|
severity = Severity.Style,
|
||||||
|
description = "Avoid using `by preferencesDataStore(...)`, use `PreferenceDataStoreFactory.create(name)`instead.",
|
||||||
|
debt = Debt.FIVE_MINS,
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun visitPropertyDelegate(delegate: KtPropertyDelegate) {
|
||||||
|
super.visitPropertyDelegate(delegate)
|
||||||
|
|
||||||
|
if (delegate.containingKtFile.fileName == "DefaultPreferencesDataStoreFactory.kt") {
|
||||||
|
// Skip the rule for the DefaultPreferencesDataStoreFactory implementation
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delegate.text.startsWith("by preferencesDataStore")) {
|
||||||
|
report(CodeSmell(
|
||||||
|
issue = issue,
|
||||||
|
entity = Entity.from(delegate),
|
||||||
|
message = "Use `PreferenceDataStoreFactory.create(name)` instead of `by preferencesDataStore(...)`."
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ class ElementRuleSetProvider : RuleSetProvider {
|
||||||
id = ruleSetId,
|
id = ruleSetId,
|
||||||
rules = listOf(
|
rules = listOf(
|
||||||
RunCatchingRule(config),
|
RunCatchingRule(config),
|
||||||
|
ByPreferencesDataStoreRule(config),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue