First Commit
This commit is contained in:
26
libraries/preferences/api/build.gradle.kts
Normal file
26
libraries/preferences/api/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
28
libraries/preferences/impl/build.gradle.kts
Normal file
28
libraries/preferences/impl/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
23
libraries/preferences/test/build.gradle.kts
Normal file
23
libraries/preferences/test/build.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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") }
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user