First Commit

This commit is contained in:
2025-12-18 16:28:50 +07:00
commit 8c3e4f491f
9974 changed files with 396488 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import extension.testCommonDependencies
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.preferences.api"
}
dependencies {
implementation(libs.coroutines.core)
implementation(projects.libraries.matrix.api)
implementation(libs.androidx.datastore.preferences)
testCommonDependencies(libs)
testImplementation(projects.libraries.preferences.test)
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.preferences.api.store
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 kotlinx.coroutines.flow.Flow
interface AppPreferencesStore {
suspend fun setDeveloperModeEnabled(enabled: Boolean)
fun isDeveloperModeEnabledFlow(): Flow<Boolean>
suspend fun setCustomElementCallBaseUrl(string: String?)
fun getCustomElementCallBaseUrlFlow(): Flow<String?>
suspend fun setTheme(theme: String)
fun getThemeFlow(): Flow<String?>
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
suspend fun setHideInviteAvatars(hide: Boolean?)
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
fun getHideInviteAvatarsFlow(): Flow<Boolean?>
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
suspend fun setTimelineMediaPreviewValue(mediaPreviewValue: MediaPreviewValue?)
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue?>
suspend fun setTracingLogLevel(logLevel: LogLevel)
fun getTracingLogLevelFlow(): Flow<LogLevel>
suspend fun setTracingLogPacks(targets: Set<TraceLogPack>)
fun getTracingLogPacksFlow(): Flow<Set<TraceLogPack>>
suspend fun reset()
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.api.store
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
/**
* Factory used to create a [DataStore] for preferences.
*
* It's a wrapper around AndroidX's `PreferenceDataStoreFactory` to make testing easier.
*/
interface PreferenceDataStoreFactory {
fun create(name: String): DataStore<Preferences>
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.api.store
import kotlinx.coroutines.flow.Flow
interface SessionPreferencesStore {
suspend fun setSharePresence(enabled: Boolean)
fun isSharePresenceEnabled(): Flow<Boolean>
suspend fun setSendPublicReadReceipts(enabled: Boolean)
fun isSendPublicReadReceiptsEnabled(): Flow<Boolean>
suspend fun setRenderReadReceipts(enabled: Boolean)
fun isRenderReadReceiptsEnabled(): Flow<Boolean>
suspend fun setSendTypingNotifications(enabled: Boolean)
fun isSendTypingNotificationsEnabled(): Flow<Boolean>
suspend fun setRenderTypingNotifications(enabled: Boolean)
fun isRenderTypingNotificationsEnabled(): Flow<Boolean>
suspend fun setSkipSessionVerification(skip: Boolean)
fun isSessionVerificationSkipped(): Flow<Boolean>
suspend fun setOptimizeImages(compress: Boolean)
fun doesOptimizeImages(): Flow<Boolean>
suspend fun setVideoCompressionPreset(preset: VideoCompressionPreset)
fun getVideoCompressionPreset(): Flow<VideoCompressionPreset>
suspend fun clear()
}

View File

@@ -0,0 +1,17 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.api.store
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.CoroutineScope
interface SessionPreferencesStoreFactory {
fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore
fun remove(sessionId: SessionId)
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* 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.preferences.api.store
/**
* Video compression presets to use when processing videos before uploading them.
*/
enum class VideoCompressionPreset {
/** High quality compression, suitable for high-resolution videos. */
HIGH,
/** Standard quality compression, suitable for most videos. */
STANDARD,
/** Low quality compression, suitable for low-resolution videos or when bandwidth is a concern. */
LOW
}

View File

@@ -0,0 +1,28 @@
import extension.setupDependencyInjection
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023, 2024 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.preferences.impl"
}
setupDependencyInjection()
dependencies {
api(projects.libraries.preferences.api)
implementation(libs.androidx.datastore.preferences)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.di)
implementation(projects.libraries.core)
implementation(projects.libraries.matrix.api)
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.preferences.impl.store
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
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
private val developerModeKey = booleanPreferencesKey("developerMode")
private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseUrl")
private val themeKey = stringPreferencesKey("theme")
private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars")
private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue")
private val logLevelKey = stringPreferencesKey("logLevel")
private val traceLogPacksKey = stringPreferencesKey("traceLogPacks")
@ContributesBinding(AppScope::class)
class DefaultAppPreferencesStore(
private val buildMeta: BuildMeta,
preferenceDataStoreFactory: PreferenceDataStoreFactory,
) : AppPreferencesStore {
private val store = preferenceDataStoreFactory.create("elementx_preferences")
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
store.edit { prefs ->
prefs[developerModeKey] = enabled
}
}
override fun isDeveloperModeEnabledFlow(): Flow<Boolean> {
return store.data.map { prefs ->
// disabled by default on release and nightly, enabled by default on debug
prefs[developerModeKey] ?: (buildMeta.buildType == BuildType.DEBUG)
}
}
override suspend fun setCustomElementCallBaseUrl(string: String?) {
store.edit { prefs ->
if (string != null) {
prefs[customElementCallBaseUrlKey] = string
} else {
prefs.remove(customElementCallBaseUrlKey)
}
}
}
override fun getCustomElementCallBaseUrlFlow(): Flow<String?> {
return store.data.map { prefs ->
prefs[customElementCallBaseUrlKey]
}
}
override suspend fun setTheme(theme: String) {
store.edit { prefs ->
prefs[themeKey] = theme
}
}
override fun getThemeFlow(): Flow<String?> {
return store.data.map { prefs ->
prefs[themeKey]
}
}
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
override fun getHideInviteAvatarsFlow(): Flow<Boolean?> {
return store.data.map { prefs ->
prefs[hideInviteAvatarsKey]
}
}
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
override suspend fun setHideInviteAvatars(hide: Boolean?) {
store.edit { prefs ->
if (hide != null) {
prefs[hideInviteAvatarsKey] = hide
} else {
prefs.remove(hideInviteAvatarsKey)
}
}
}
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
override suspend fun setTimelineMediaPreviewValue(mediaPreviewValue: MediaPreviewValue?) {
store.edit { prefs ->
if (mediaPreviewValue != null) {
prefs[timelineMediaPreviewValueKey] = mediaPreviewValue.name
} else {
prefs.remove(timelineMediaPreviewValueKey)
}
}
}
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
override fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue?> {
return store.data.map { prefs ->
prefs[timelineMediaPreviewValueKey]?.let { MediaPreviewValue.valueOf(it) }
}
}
override suspend fun setTracingLogLevel(logLevel: LogLevel) {
store.edit { prefs ->
prefs[logLevelKey] = logLevel.name
}
}
override fun getTracingLogLevelFlow(): Flow<LogLevel> {
return store.data.map { prefs ->
prefs[logLevelKey]?.let { LogLevel.valueOf(it) } ?: buildMeta.defaultLogLevel()
}
}
override suspend fun setTracingLogPacks(targets: Set<TraceLogPack>) {
val value = targets.joinToString(",") { it.key }
store.edit { prefs ->
prefs[traceLogPacksKey] = value
}
}
override fun getTracingLogPacksFlow(): Flow<Set<TraceLogPack>> {
return store.data.map { prefs ->
prefs[traceLogPacksKey]
?.split(",")
?.mapNotNull { value -> TraceLogPack.entries.find { it.key == value } }
?.toSet()
?: emptySet()
}
}
override suspend fun reset() {
store.edit { it.clear() }
}
}
private fun BuildMeta.defaultLogLevel(): LogLevel {
return when (buildType) {
BuildType.DEBUG -> LogLevel.TRACE
BuildType.NIGHTLY -> LogLevel.DEBUG
BuildType.RELEASE -> LogLevel.INFO
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.impl.store
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.androidutils.preferences.DefaultPreferencesCorruptionHandlerFactory
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import java.util.concurrent.ConcurrentHashMap
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultPreferencesDataStoreFactory(
@ApplicationContext private val context: Context,
) : PreferenceDataStoreFactory {
private val dataStoreHolders = ConcurrentHashMap<String, DataStoreHolder>()
private class DataStoreHolder(name: String) {
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = name,
corruptionHandler = DefaultPreferencesCorruptionHandlerFactory.replaceWithEmpty(),
)
}
override fun create(name: String): DataStore<Preferences> {
val holder = dataStoreHolders.getOrPut(name) {
DataStoreHolder(name)
}
return with(holder) {
context.dataStore
}
}
}

View File

@@ -0,0 +1,108 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.impl.store
import android.content.Context
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
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.preferencesDataStoreFile
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.androidutils.hash.hash
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.io.File
class DefaultSessionPreferencesStore(
context: Context,
sessionId: SessionId,
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
) : SessionPreferencesStore {
companion object {
fun storeFile(context: Context, sessionId: SessionId): File {
val hashedUserId = sessionId.value.hash().take(16)
return context.preferencesDataStoreFile("session_${hashedUserId}_preferences")
}
}
private val sharePresenceKey = booleanPreferencesKey("sharePresence")
private val sendPublicReadReceiptsKey = booleanPreferencesKey("sendPublicReadReceipts")
private val renderReadReceiptsKey = booleanPreferencesKey("renderReadReceipts")
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
private val compressImages = booleanPreferencesKey("compressMedia")
private val compressMediaPreset = stringPreferencesKey("compressMediaPreset")
private val dataStoreFile = storeFile(context, sessionId)
private val store = PreferenceDataStoreFactory.create(
scope = sessionCoroutineScope,
migrations = listOf(
SessionPreferencesStoreMigration(
sharePresenceKey,
sendPublicReadReceiptsKey,
)
),
) { dataStoreFile }
override suspend fun setSharePresence(enabled: Boolean) {
update(sharePresenceKey, enabled)
// Also update all the other settings
setSendPublicReadReceipts(enabled)
setRenderReadReceipts(enabled)
setSendTypingNotifications(enabled)
setRenderTypingNotifications(enabled)
}
override fun isSharePresenceEnabled(): Flow<Boolean> {
return get(sharePresenceKey) { true }
}
override suspend fun setSendPublicReadReceipts(enabled: Boolean) = update(sendPublicReadReceiptsKey, enabled)
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = get(sendPublicReadReceiptsKey) { true }
override suspend fun setRenderReadReceipts(enabled: Boolean) = update(renderReadReceiptsKey, enabled)
override fun isRenderReadReceiptsEnabled(): Flow<Boolean> = get(renderReadReceiptsKey) { true }
override suspend fun setSendTypingNotifications(enabled: Boolean) = update(sendTypingNotificationsKey, enabled)
override fun isSendTypingNotificationsEnabled(): Flow<Boolean> = get(sendTypingNotificationsKey) { true }
override suspend fun setRenderTypingNotifications(enabled: Boolean) = update(renderTypingNotificationsKey, enabled)
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = get(renderTypingNotificationsKey) { true }
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
override suspend fun setOptimizeImages(compress: Boolean) = update(compressImages, compress)
override fun doesOptimizeImages(): Flow<Boolean> = get(compressImages) { true }
override suspend fun setVideoCompressionPreset(preset: VideoCompressionPreset) = update(compressMediaPreset, preset.name)
override fun getVideoCompressionPreset(): Flow<VideoCompressionPreset> = get(compressMediaPreset) { VideoCompressionPreset.STANDARD.name }
.map { tryOrNull { VideoCompressionPreset.valueOf(it) } ?: VideoCompressionPreset.STANDARD }
override suspend fun clear() {
dataStoreFile.safeDelete()
}
private suspend fun <T> update(key: Preferences.Key<T>, value: T) {
store.edit { prefs -> prefs[key] = value }
}
private fun <T> get(key: Preferences.Key<T>, default: () -> T): Flow<T> {
return store.data.map { prefs -> prefs[key] ?: default() }
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.impl.store
import android.content.Context
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn
import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
import kotlinx.coroutines.CoroutineScope
import java.util.concurrent.ConcurrentHashMap
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultSessionPreferencesStoreFactory(
@ApplicationContext private val context: Context,
sessionObserver: SessionObserver,
) : SessionPreferencesStoreFactory {
private val cache = ConcurrentHashMap<SessionId, DefaultSessionPreferencesStore>()
init {
sessionObserver.addListener(object : SessionListener {
override suspend fun onSessionDeleted(userId: String, wasLastSession: Boolean) {
val sessionPreferences = cache.remove(SessionId(userId))
sessionPreferences?.clear()
}
})
}
override fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore = cache.getOrPut(sessionId) {
DefaultSessionPreferencesStore(context, sessionId, sessionCoroutineScope)
}
override fun remove(sessionId: SessionId) {
cache.remove(sessionId)
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.impl.store
import dev.zacsweers.metro.BindingContainer
import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Provides
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import kotlinx.coroutines.CoroutineScope
@BindingContainer
@ContributesTo(SessionScope::class)
object SessionPreferencesModule {
@Provides
fun providesSessionPreferencesStore(
defaultSessionPreferencesStoreFactory: DefaultSessionPreferencesStoreFactory,
sessionId: SessionId,
@SessionCoroutineScope sessionCoroutineScope: CoroutineScope,
): SessionPreferencesStore {
return defaultSessionPreferencesStoreFactory
.get(sessionId, sessionCoroutineScope)
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.impl.store
import androidx.datastore.core.DataMigration
import androidx.datastore.preferences.core.Preferences
class SessionPreferencesStoreMigration(
private val sharePresenceKey: Preferences.Key<Boolean>,
private val sendPublicReadReceiptsKey: Preferences.Key<Boolean>,
) : DataMigration<Preferences> {
override suspend fun cleanUp() = Unit
override suspend fun shouldMigrate(currentData: Preferences): Boolean {
return currentData[sharePresenceKey] == null
}
override suspend fun migrate(currentData: Preferences): Preferences {
// If sendPublicReadReceiptsKey was false, consider that sharing presence is false.
val defaultValue = currentData[sendPublicReadReceiptsKey] ?: true
return currentData.toMutablePreferences().apply {
set(sharePresenceKey, defaultValue)
}.toPreferences()
}
}

View File

@@ -0,0 +1,23 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.libraries.preferences.test"
}
dependencies {
api(projects.libraries.preferences.api)
implementation(projects.libraries.matrix.api)
implementation(projects.tests.testutils)
implementation(libs.coroutines.core)
implementation(libs.androidx.datastore.preferences)
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.test
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
import java.io.File
import androidx.datastore.preferences.core.PreferenceDataStoreFactory as AndroidPreferenceDataStoreFactory
class FakePreferenceDataStoreFactory : PreferenceDataStoreFactory {
override fun create(name: String): DataStore<Preferences> {
return AndroidPreferenceDataStoreFactory.create { File.createTempFile("test", ".preferences_pb") }
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.test
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.SessionPreferencesStoreFactory
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
import io.element.android.tests.testutils.lambda.LambdaTwoParamsRecorder
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.CoroutineScope
class FakeSessionPreferencesStoreFactory(
val getLambda: LambdaTwoParamsRecorder<SessionId, CoroutineScope, SessionPreferencesStore> = lambdaRecorder { _, _ -> lambdaError() },
val removeLambda: LambdaOneParamRecorder<SessionId, Unit> = lambdaRecorder { _ -> lambdaError() },
) : SessionPreferencesStoreFactory {
override fun get(sessionId: SessionId, sessionCoroutineScope: CoroutineScope): SessionPreferencesStore {
return getLambda(sessionId, sessionCoroutineScope)
}
override fun remove(sessionId: SessionId) {
removeLambda(sessionId)
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-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.preferences.test
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 kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class InMemoryAppPreferencesStore(
isDeveloperModeEnabled: Boolean = false,
customElementCallBaseUrl: String? = null,
hideInviteAvatars: Boolean? = null,
timelineMediaPreviewValue: MediaPreviewValue? = null,
theme: String? = null,
logLevel: LogLevel = LogLevel.INFO,
traceLockPacks: Set<TraceLogPack> = emptySet(),
) : AppPreferencesStore {
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
private val theme = MutableStateFlow(theme)
private val logLevel = MutableStateFlow(logLevel)
private val tracingLogPacks = MutableStateFlow(traceLockPacks)
private val hideInviteAvatars = MutableStateFlow(hideInviteAvatars)
private val timelineMediaPreviewValue = MutableStateFlow(timelineMediaPreviewValue)
override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
isDeveloperModeEnabled.value = enabled
}
override fun isDeveloperModeEnabledFlow(): Flow<Boolean> {
return isDeveloperModeEnabled
}
override suspend fun setCustomElementCallBaseUrl(string: String?) {
customElementCallBaseUrl.tryEmit(string)
}
override fun getCustomElementCallBaseUrlFlow(): Flow<String?> {
return customElementCallBaseUrl
}
override suspend fun setTheme(theme: String) {
this.theme.value = theme
}
override fun getThemeFlow(): Flow<String?> {
return theme
}
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
override fun getHideInviteAvatarsFlow(): Flow<Boolean?> {
return hideInviteAvatars
}
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
override fun getTimelineMediaPreviewValueFlow(): Flow<MediaPreviewValue?> {
return timelineMediaPreviewValue
}
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
override suspend fun setHideInviteAvatars(hide: Boolean?) {
hideInviteAvatars.value = hide
}
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
override suspend fun setTimelineMediaPreviewValue(mediaPreviewValue: MediaPreviewValue?) {
timelineMediaPreviewValue.value = mediaPreviewValue
}
override suspend fun setTracingLogLevel(logLevel: LogLevel) {
this.logLevel.value = logLevel
}
override fun getTracingLogLevelFlow(): Flow<LogLevel> {
return logLevel
}
override suspend fun setTracingLogPacks(targets: Set<TraceLogPack>) {
tracingLogPacks.value = targets
}
override fun getTracingLogPacksFlow(): Flow<Set<TraceLogPack>> {
return tracingLogPacks
}
override suspend fun reset() {
// No op
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2024, 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.preferences.test
import io.element.android.libraries.preferences.api.store.SessionPreferencesStore
import io.element.android.libraries.preferences.api.store.VideoCompressionPreset
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class InMemorySessionPreferencesStore(
isSharePresenceEnabled: Boolean = true,
isSendPublicReadReceiptsEnabled: Boolean = true,
isRenderReadReceiptsEnabled: Boolean = true,
isSendTypingNotificationsEnabled: Boolean = true,
isRenderTypingNotificationsEnabled: Boolean = true,
isSessionVerificationSkipped: Boolean = false,
doesCompressMedia: Boolean = true,
videoCompressionPreset: VideoCompressionPreset = VideoCompressionPreset.STANDARD,
) : SessionPreferencesStore {
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
private val isRenderReadReceiptsEnabled = MutableStateFlow(isRenderReadReceiptsEnabled)
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
private val doesCompressMedia = MutableStateFlow(doesCompressMedia)
private val videoCompressionPreset = MutableStateFlow(videoCompressionPreset)
var clearCallCount = 0
private set
override suspend fun setSharePresence(enabled: Boolean) {
isSharePresenceEnabled.tryEmit(enabled)
}
override fun isSharePresenceEnabled(): Flow<Boolean> = isSharePresenceEnabled
override suspend fun setSendPublicReadReceipts(enabled: Boolean) {
isSendPublicReadReceiptsEnabled.tryEmit(enabled)
}
override fun isSendPublicReadReceiptsEnabled(): Flow<Boolean> = isSendPublicReadReceiptsEnabled
override suspend fun setRenderReadReceipts(enabled: Boolean) {
isRenderReadReceiptsEnabled.tryEmit(enabled)
}
override fun isRenderReadReceiptsEnabled(): Flow<Boolean> = isRenderReadReceiptsEnabled
override suspend fun setSendTypingNotifications(enabled: Boolean) {
isSendTypingNotificationsEnabled.tryEmit(enabled)
}
override fun isSendTypingNotificationsEnabled(): Flow<Boolean> = isSendTypingNotificationsEnabled
override suspend fun setRenderTypingNotifications(enabled: Boolean) {
isRenderTypingNotificationsEnabled.tryEmit(enabled)
}
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = isRenderTypingNotificationsEnabled
override suspend fun setSkipSessionVerification(skip: Boolean) {
isSessionVerificationSkipped.tryEmit(skip)
}
override fun isSessionVerificationSkipped(): Flow<Boolean> {
return isSessionVerificationSkipped
}
override suspend fun setOptimizeImages(compress: Boolean) = doesCompressMedia.emit(compress)
override fun doesOptimizeImages(): Flow<Boolean> = doesCompressMedia
override suspend fun setVideoCompressionPreset(preset: VideoCompressionPreset) {
videoCompressionPreset.value = preset
}
override fun getVideoCompressionPreset(): Flow<VideoCompressionPreset> {
return videoCompressionPreset
}
override suspend fun clear() {
clearCallCount++
isSendPublicReadReceiptsEnabled.tryEmit(true)
}
}