diff --git a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt index 72e864bbea..d2fbb1b78f 100644 --- a/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt +++ b/app/src/main/kotlin/io/element/android/x/initializer/CrashInitializer.kt @@ -10,10 +10,14 @@ package io.element.android.x.initializer import android.content.Context import androidx.startup.Initializer 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 { override fun create(context: Context) { - VectorUncaughtExceptionHandler(context).activate() + VectorUncaughtExceptionHandler( + context.bindings().preferencesCrashDataStore(), + ).activate() } override fun dependencies(): List>> = emptyList() diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index ad77b20e60..4260e9e792 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -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) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt index b3086ddb9e..a2ccccf984 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt +++ b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/storage/PreferencesLockScreenStore.kt @@ -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 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 { - return context.dataStore.data.map { preferences -> + return dataStore.data.map { preferences -> preferences[pinCodeKey] != null } } override fun isBiometricUnlockAllowed(): Flow { - 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 } } diff --git a/features/migration/impl/build.gradle.kts b/features/migration/impl/build.gradle.kts index 878cf40d3a..12bf396e74 100644 --- a/features/migration/impl/build.gradle.kts +++ b/features/migration/impl/build.gradle.kts @@ -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) diff --git a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt index a79a073022..61a9ba69a4 100644 --- a/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt +++ b/features/migration/impl/src/main/kotlin/io/element/android/features/migration/impl/DefaultMigrationStore.kt @@ -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 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 -> diff --git a/features/rageshake/impl/build.gradle.kts b/features/rageshake/impl/build.gradle.kts index addfe3d794..0a3b4577f0 100644 --- a/features/rageshake/impl/build.gradle.kts +++ b/features/rageshake/impl/build.gradle.kts @@ -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) diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt index 0b86bb4ef0..88f3b5ce0e 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/PreferencesCrashDataStore.kt @@ -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 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 diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt index ca310cedad..e41583b61c 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandler.kt @@ -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) } diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.kt new file mode 100644 index 0000000000..dc603f7b8f --- /dev/null +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/di/RageshakeBindings.kt @@ -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 +} diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt index 9d7171b8a0..0642f20e12 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/rageshake/PreferencesRageshakeDataStore.kt @@ -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 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 { return store.data.map { prefs -> diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt index f9f1aa72e4..f19362f99c 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/crash/VectorUncaughtExceptionHandlerTest.kt @@ -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() diff --git a/gradle.properties b/gradle.properties index 1c4ccb38eb..38cf7488a4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -54,3 +54,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 diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts index 1aa23a09e8..0c58f6ce8c 100644 --- a/libraries/androidutils/build.gradle.kts +++ b/libraries/androidutils/build.gradle.kts @@ -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) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.kt new file mode 100644 index 0000000000..e6270f4af3 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/preferences/DefaultPreferencesCorruptionHandlerFactory.kt @@ -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 { + return ReplaceFileCorruptionHandler( + produceNewData = { + // If the preferences file is corrupted, we return an empty preferences object + emptyPreferences() + }, + ) + } +} diff --git a/libraries/featureflag/impl/build.gradle.kts b/libraries/featureflag/impl/build.gradle.kts index c1da63dba3..bd702fafac 100644 --- a/libraries/featureflag/impl/build.gradle.kts +++ b/libraries/featureflag/impl/build.gradle.kts @@ -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) diff --git a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt index 0c1f1a45e8..357bf3c548 100644 --- a/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt +++ b/libraries/featureflag/impl/src/main/kotlin/io/element/android/libraries/featureflag/impl/PreferencesFeatureFlagProvider.kt @@ -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 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 diff --git a/libraries/permissions/impl/build.gradle.kts b/libraries/permissions/impl/build.gradle.kts index 2b38e54ef8..347032c65e 100644 --- a/libraries/permissions/impl/build.gradle.kts +++ b/libraries/permissions/impl/build.gradle.kts @@ -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) diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt index ae9eee76e9..4d55e6b9f9 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsStore.kt @@ -7,28 +7,22 @@ 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.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 by preferencesDataStore(name = "permissions_store") - @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 -> diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt index 0dff16ff98..453b1304f3 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultAppPreferencesStore.kt @@ -7,28 +7,22 @@ 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.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType 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.tracing.LogLevel 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.PreferenceDataStoreFactory import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore(name = "elementx_preferences") - private val developerModeKey = booleanPreferencesKey("developerMode") private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl") private val themeKey = stringPreferencesKey("theme") @@ -39,10 +33,10 @@ private val traceLogPacksKey = stringPreferencesKey("traceLogPacks") @ContributesBinding(AppScope::class) class DefaultAppPreferencesStore @Inject constructor( - @ApplicationContext context: Context, private val buildMeta: BuildMeta, + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : AppPreferencesStore { - private val store = context.dataStore + private val store = preferenceDataStoreFactory.create("elementx_preferences") override suspend fun setDeveloperModeEnabled(enabled: Boolean) { store.edit { prefs -> diff --git a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesDataStoreFactory.kt b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesDataStoreFactory.kt index b208492150..f2631e4a3a 100644 --- a/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesDataStoreFactory.kt +++ b/libraries/preferences/impl/src/main/kotlin/io/element/android/libraries/preferences/impl/store/DefaultPreferencesDataStoreFactory.kt @@ -12,6 +12,7 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.preferencesDataStore 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.ApplicationContext import io.element.android.libraries.di.SingleIn @@ -27,7 +28,10 @@ class DefaultPreferencesDataStoreFactory @Inject constructor( private val dataStoreHolders = ConcurrentHashMap() private class DataStoreHolder(name: String) { - val Context.dataStore: DataStore by preferencesDataStore(name = name) + val Context.dataStore: DataStore by preferencesDataStore( + name = name, + corruptionHandler = DefaultPreferencesCorruptionHandlerFactory.replaceWithEmpty(), + ) } override fun create(name: String): DataStore { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt index a4d9413cb1..2f67e04b3d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt @@ -7,12 +7,8 @@ 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.intPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList 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.DateFormatterMode 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.RoomId 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.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.map import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store") - -@SingleIn(AppScope::class) @ContributesBinding(AppScope::class) class DefaultPushDataStore @Inject constructor( - @ApplicationContext private val context: Context, private val pushDatabase: PushDatabase, private val dateFormatter: DateFormatter, private val dispatchers: CoroutineDispatchers, + preferencesFactory: PreferenceDataStoreFactory, ) : PushDataStore { private val pushCounter = intPreferencesKey("push_counter") + private val dataStore = preferencesFactory.create("push_store") + /** * Integer preference to track the state of the battery optimization banner. * Possible values: @@ -52,24 +49,24 @@ class DefaultPushDataStore @Inject constructor( */ private val batteryOptimizationBannerState = intPreferencesKey("battery_optimization_banner_state") - override val pushCounterFlow: Flow = context.dataStore.data.map { preferences -> + override val pushCounterFlow: Flow = dataStore.data.map { preferences -> preferences[pushCounter] ?: 0 } @Suppress("UnnecessaryParentheses") - override val shouldDisplayBatteryOptimizationBannerFlow: Flow = context.dataStore.data.map { preferences -> + override val shouldDisplayBatteryOptimizationBannerFlow: Flow = dataStore.data.map { preferences -> (preferences[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT) == BATTERY_OPTIMIZATION_BANNER_STATE_SHOW } suspend fun incrementPushCounter() { - context.dataStore.edit { settings -> + dataStore.edit { settings -> val currentCounterValue = settings[pushCounter] ?: 0 settings[pushCounter] = currentCounterValue + 1 } } suspend fun setBatteryOptimizationBannerState(newState: Int) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> val currentValue = settings[batteryOptimizationBannerState] ?: BATTERY_OPTIMIZATION_BANNER_STATE_INIT settings[batteryOptimizationBannerState] = when (currentValue) { BATTERY_OPTIMIZATION_BANNER_STATE_INIT, @@ -106,7 +103,7 @@ class DefaultPushDataStore @Inject constructor( override suspend fun reset() { pushDatabase.pushHistoryQueries.removeAll() - context.dataStore.edit { + dataStore.edit { it.clear() } } diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts index ec81643cfd..3bda0aaaed 100644 --- a/libraries/pushstore/impl/build.gradle.kts +++ b/libraries/pushstore/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.pushstore.api) implementation(libs.androidx.corektx) implementation(libs.androidx.datastore.preferences) @@ -38,6 +39,7 @@ dependencies { testImplementation(libs.test.turbine) testImplementation(libs.coroutines.test) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.libraries.preferences.test) testImplementation(projects.services.appnavstate.test) testImplementation(projects.libraries.pushstore.test) diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index 8003449c95..aef5c94da2 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -13,6 +13,7 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStoreFactory import java.util.concurrent.ConcurrentHashMap @@ -22,6 +23,7 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultUserPushStoreFactory @Inject constructor( @ApplicationContext private val context: Context, + private val preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : UserPushStoreFactory { // We can have only one class accessing a single data store, so keep a cache of them. private val cache = ConcurrentHashMap() @@ -29,7 +31,8 @@ class DefaultUserPushStoreFactory @Inject constructor( return cache.getOrPut(userId) { UserPushStoreDataStore( context = context, - userId = userId + userId = userId, + factory = preferenceDataStoreFactory, ) } } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 1ab555bba3..399b9648f4 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -13,12 +13,12 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStoreFile import io.element.android.libraries.androidutils.hash.hash import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -31,6 +31,7 @@ import timber.log.Timber class UserPushStoreDataStore( private val context: Context, userId: SessionId, + factory: PreferenceDataStoreFactory, ) : UserPushStore { // Hash the sessionId to get rid of exotic chars and take only the first 16 chars. // The risk of collision is not high. @@ -49,28 +50,28 @@ class UserPushStoreDataStore( } } - private val Context.dataStore: DataStore by preferencesDataStore(name = preferenceName) + private val store: DataStore = factory.create(preferenceName) private val pushProviderName = stringPreferencesKey("pushProviderName") private val currentPushKey = stringPreferencesKey("currentPushKey") private val notificationEnabled = booleanPreferencesKey("notificationEnabled") private val ignoreRegistrationError = booleanPreferencesKey("ignoreRegistrationError") override suspend fun getPushProviderName(): String? { - return context.dataStore.data.first()[pushProviderName] + return store.data.first()[pushProviderName] } override suspend fun setPushProviderName(value: String) { - context.dataStore.edit { + store.edit { it[pushProviderName] = value } } override suspend fun getCurrentRegisteredPushKey(): String? { - return context.dataStore.data.first()[currentPushKey] + return store.data.first()[currentPushKey] } override suspend fun setCurrentRegisteredPushKey(value: String?) { - context.dataStore.edit { + store.edit { if (value == null) { it.remove(currentPushKey) } else { @@ -80,11 +81,11 @@ class UserPushStoreDataStore( } override fun getNotificationEnabledForDevice(): Flow { - return context.dataStore.data.map { it[notificationEnabled].orTrue() } + return store.data.map { it[notificationEnabled].orTrue() } } override suspend fun setNotificationEnabledForDevice(enabled: Boolean) { - context.dataStore.edit { + store.edit { it[notificationEnabled] = enabled } } @@ -94,17 +95,17 @@ class UserPushStoreDataStore( } override fun ignoreRegistrationError(): Flow { - return context.dataStore.data.map { it[ignoreRegistrationError].orFalse() } + return store.data.map { it[ignoreRegistrationError].orFalse() } } override suspend fun setIgnoreRegistrationError(ignore: Boolean) { - context.dataStore.edit { + store.edit { it[ignoreRegistrationError] = ignore } } override suspend fun reset() { - context.dataStore.edit { + store.edit { it.clear() } // Also delete the file diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DataStorePushClientSecretStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DataStorePushClientSecretStore.kt index 6a8cbd543a..7728b0847a 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DataStorePushClientSecretStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/DataStorePushClientSecretStore.kt @@ -7,44 +7,40 @@ package io.element.android.libraries.pushstore.impl.clientsecret -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore import kotlinx.coroutines.flow.first import javax.inject.Inject -private val Context.dataStore: DataStore by preferencesDataStore(name = "push_client_secret_store") - @ContributesBinding(AppScope::class) class DataStorePushClientSecretStore @Inject constructor( - @ApplicationContext private val context: Context, + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : PushClientSecretStore { + private val dataStore = preferenceDataStoreFactory.create("push_client_secret_store") + override suspend fun storeSecret(userId: SessionId, clientSecret: String) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[getPreferenceKeyForUser(userId)] = clientSecret } } override suspend fun getSecret(userId: SessionId): String? { - return context.dataStore.data.first()[getPreferenceKeyForUser(userId)] + return dataStore.data.first()[getPreferenceKeyForUser(userId)] } override suspend fun resetSecret(userId: SessionId) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings.remove(getPreferenceKeyForUser(userId)) } } override suspend fun getUserIdFromSecret(clientSecret: String): SessionId? { - val keyValues = context.dataStore.data.first().asMap() + val keyValues = dataStore.data.first().asMap() val matchingKey = keyValues.keys.find { keyValues[it] == clientSecret } diff --git a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt index 1ba668073a..2c1a10a333 100644 --- a/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStoreTest.kt @@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID_2 +import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import org.junit.Test @@ -92,5 +93,6 @@ class UserPushStoreDataStoreTest { ) = UserPushStoreDataStore( context = InstrumentationRegistry.getInstrumentation().context, userId = sessionId, + factory = FakePreferenceDataStoreFactory(), ) } diff --git a/services/analytics/impl/build.gradle.kts b/services/analytics/impl/build.gradle.kts index 3ccd7a18c8..6143511f26 100644 --- a/services/analytics/impl/build.gradle.kts +++ b/services/analytics/impl/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.designsystem) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.sessionStorage.api) api(projects.services.analyticsproviders.api) diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt index 7c7109e482..446d3fdaaa 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/store/AnalyticsStore.kt @@ -7,27 +7,18 @@ 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.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.distinctUntilChanged import kotlinx.coroutines.flow.map import javax.inject.Inject -/** - * Also accessed via reflection by the instrumentation tests @see [im.vector.app.ClearCurrentSessionRule]. - */ -private val Context.dataStore: DataStore by preferencesDataStore(name = "vector_analytics") - /** * Local storage for: * - user consent (Boolean); @@ -46,44 +37,46 @@ interface AnalyticsStore { @ContributesBinding(AppScope::class) class DefaultAnalyticsStore @Inject constructor( - @ApplicationContext private val context: Context + preferenceDataStoreFactory: PreferenceDataStoreFactory, ) : AnalyticsStore { private val userConsent = booleanPreferencesKey("user_consent") private val didAskUserConsent = booleanPreferencesKey("did_ask_user_consent") private val analyticsId = stringPreferencesKey("analytics_id") - override val userConsentFlow: Flow = context.dataStore.data + private val dataStore = preferenceDataStoreFactory.create("vector_analytics") + + override val userConsentFlow: Flow = dataStore.data .map { preferences -> preferences[userConsent].orFalse() } .distinctUntilChanged() - override val didAskUserConsentFlow: Flow = context.dataStore.data + override val didAskUserConsentFlow: Flow = dataStore.data .map { preferences -> preferences[didAskUserConsent].orFalse() } .distinctUntilChanged() - override val analyticsIdFlow: Flow = context.dataStore.data + override val analyticsIdFlow: Flow = dataStore.data .map { preferences -> preferences[analyticsId].orEmpty() } .distinctUntilChanged() override suspend fun setUserConsent(newUserConsent: Boolean) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[userConsent] = newUserConsent } } override suspend fun setDidAskUserConsent(newValue: Boolean) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[didAskUserConsent] = newValue } } override suspend fun setAnalyticsId(newAnalyticsId: String) { - context.dataStore.edit { settings -> + dataStore.edit { settings -> settings[analyticsId] = newAnalyticsId } } override suspend fun reset() { - context.dataStore.edit { + dataStore.edit { it.clear() } } diff --git a/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ByPreferencesDataStoreRule.kt b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ByPreferencesDataStoreRule.kt new file mode 100644 index 0000000000..cb00dc18b1 --- /dev/null +++ b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ByPreferencesDataStoreRule.kt @@ -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(...)`." + )) + } + } +} diff --git a/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt index 8e0ed3ee50..9a7ba55dc6 100644 --- a/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt +++ b/tests/detekt-rules/src/main/kotlin/io/element/android/detektrules/ElementRuleSetProvider.kt @@ -18,6 +18,7 @@ class ElementRuleSetProvider : RuleSetProvider { id = ruleSetId, rules = listOf( RunCatchingRule(config), + ByPreferencesDataStoreRule(config), ) ) }