Merge pull request #4158 from element-hq/feature/fga/update_rust_sdk_0.2.75

update rust sdk 0.2.75
This commit is contained in:
ganfra 2025-01-17 17:22:19 +01:00 committed by GitHub
commit 9165b18185
60 changed files with 479 additions and 1017 deletions

View file

@ -9,50 +9,34 @@ package io.element.android.x.initializer
import android.content.Context import android.content.Context
import android.system.Os import android.system.Os
import androidx.preference.PreferenceManager
import androidx.startup.Initializer import androidx.startup.Initializer
import io.element.android.features.preferences.impl.developer.tracing.SharedPreferencesTracingConfigurationStore
import io.element.android.features.preferences.impl.developer.tracing.TargetLogLevelMapBuilder
import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import io.element.android.x.BuildConfig import io.element.android.x.BuildConfig
import io.element.android.x.di.AppBindings import io.element.android.x.di.AppBindings
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
private const val ELEMENT_X_TARGET = "elementx"
class TracingInitializer : Initializer<Unit> { class TracingInitializer : Initializer<Unit> {
override fun create(context: Context) { override fun create(context: Context) {
val appBindings = context.bindings<AppBindings>() val appBindings = context.bindings<AppBindings>()
val tracingService = appBindings.tracingService() val tracingService = appBindings.tracingService()
val bugReporter = appBindings.bugReporter() val bugReporter = appBindings.bugReporter()
Timber.plant(tracingService.createTimberTree()) Timber.plant(tracingService.createTimberTree(ELEMENT_X_TARGET))
val tracingConfiguration = if (BuildConfig.BUILD_TYPE == BuildType.RELEASE.name) { val preferencesStore = appBindings.preferencesStore()
TracingConfiguration( val logLevel = runBlocking { preferencesStore.getTracingLogLevelFlow().first() }
filterConfiguration = TracingFilterConfigurations.release, val tracingConfiguration = TracingConfiguration(
writesToLogcat = false, writesToLogcat = BuildConfig.DEBUG,
writesToFilesConfiguration = defaultWriteToDiskConfiguration(bugReporter), writesToFilesConfiguration = defaultWriteToDiskConfiguration(bugReporter),
) logLevel = logLevel,
} else { extraTargets = listOf(ELEMENT_X_TARGET),
val prefs = PreferenceManager.getDefaultSharedPreferences(context) )
val store = SharedPreferencesTracingConfigurationStore(prefs) bugReporter.setCurrentTracingLogLevel(logLevel.name)
val builder = TargetLogLevelMapBuilder(
tracingConfigurationStore = store,
defaultConfig = if (BuildConfig.BUILD_TYPE == BuildType.NIGHTLY.name) {
TracingFilterConfigurations.nightly
} else {
TracingFilterConfigurations.debug
}
)
TracingConfiguration(
filterConfiguration = TracingFilterConfigurations.custom(builder.getCurrentMap()),
writesToLogcat = BuildConfig.DEBUG,
writesToFilesConfiguration = defaultWriteToDiskConfiguration(bugReporter),
)
}
bugReporter.setCurrentTracingFilter(tracingConfiguration.filterConfiguration.filter)
tracingService.setupTracing(tracingConfiguration) tracingService.setupTracing(tracingConfiguration)
// Also set env variable for rust back trace // Also set env variable for rust back trace
Os.setenv("RUST_BACKTRACE", "1", true) Os.setenv("RUST_BACKTRACE", "1", true)

View file

@ -24,7 +24,6 @@ import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.login.api.LoginEntryPoint import io.element.android.features.login.api.LoginEntryPoint
import io.element.android.features.login.api.LoginFlowType import io.element.android.features.login.api.LoginFlowType
import io.element.android.features.onboarding.api.OnBoardingEntryPoint import io.element.android.features.onboarding.api.OnBoardingEntryPoint
import io.element.android.features.preferences.api.ConfigureTracingEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices import io.element.android.libraries.designsystem.utils.ForceOrientationInMobileDevices
@ -38,7 +37,6 @@ class NotLoggedInFlowNode @AssistedInject constructor(
@Assisted buildContext: BuildContext, @Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val onBoardingEntryPoint: OnBoardingEntryPoint, private val onBoardingEntryPoint: OnBoardingEntryPoint,
private val configureTracingEntryPoint: ConfigureTracingEntryPoint,
private val loginEntryPoint: LoginEntryPoint, private val loginEntryPoint: LoginEntryPoint,
private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory, private val notLoggedInImageLoaderFactory: NotLoggedInImageLoaderFactory,
) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>( ) : BaseFlowNode<NotLoggedInFlowNode.NavTarget>(
@ -68,9 +66,6 @@ class NotLoggedInFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data class LoginFlow(val type: LoginFlowType) : NavTarget data class LoginFlow(val type: LoginFlowType) : NavTarget
@Parcelize
data object ConfigureTracing : NavTarget
} }
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -89,10 +84,6 @@ class NotLoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_IN_QR_CODE)) backstack.push(NavTarget.LoginFlow(type = LoginFlowType.SIGN_IN_QR_CODE))
} }
override fun onOpenDeveloperSettings() {
backstack.push(NavTarget.ConfigureTracing)
}
override fun onReportProblem() { override fun onReportProblem() {
plugins<Callback>().forEach { it.onOpenBugReport() } plugins<Callback>().forEach { it.onOpenBugReport() }
} }
@ -107,9 +98,6 @@ class NotLoggedInFlowNode @AssistedInject constructor(
.params(LoginEntryPoint.Params(flowType = navTarget.type)) .params(LoginEntryPoint.Params(flowType = navTarget.type))
.build() .build()
} }
NavTarget.ConfigureTracing -> {
configureTracingEntryPoint.createNode(this, buildContext)
}
} }
} }

View file

@ -24,7 +24,6 @@ interface OnBoardingEntryPoint : FeatureEntryPoint {
fun onSignUp() fun onSignUp()
fun onSignIn() fun onSignIn()
fun onSignInWithQrCode() fun onSignInWithQrCode()
fun onOpenDeveloperSettings()
fun onReportProblem() fun onReportProblem()
} }
} }

View file

@ -40,10 +40,6 @@ class OnBoardingNode @AssistedInject constructor(
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onSignInWithQrCode() } plugins<OnBoardingEntryPoint.Callback>().forEach { it.onSignInWithQrCode() }
} }
private fun onOpenDeveloperSettings() {
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onOpenDeveloperSettings() }
}
private fun onReportProblem() { private fun onReportProblem() {
plugins<OnBoardingEntryPoint.Callback>().forEach { it.onReportProblem() } plugins<OnBoardingEntryPoint.Callback>().forEach { it.onReportProblem() }
} }
@ -57,7 +53,6 @@ class OnBoardingNode @AssistedInject constructor(
onSignIn = ::onSignIn, onSignIn = ::onSignIn,
onCreateAccount = ::onSignUp, onCreateAccount = ::onSignUp,
onSignInWithQrCode = ::onSignInWithQrCode, onSignInWithQrCode = ::onSignInWithQrCode,
onOpenDeveloperSettings = ::onOpenDeveloperSettings,
onReportProblem = ::onReportProblem, onReportProblem = ::onReportProblem,
) )
} }

View file

@ -13,7 +13,6 @@ import androidx.compose.runtime.produceState
import io.element.android.appconfig.OnBoardingConfig import io.element.android.appconfig.OnBoardingConfig
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import javax.inject.Inject import javax.inject.Inject
@ -31,8 +30,7 @@ class OnBoardingPresenter @Inject constructor(
val canLoginWithQrCode by produceState(initialValue = false) { val canLoginWithQrCode by produceState(initialValue = false) {
value = featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin) value = featureFlagService.isFeatureEnabled(FeatureFlags.QrCodeLogin)
} }
return OnBoardingState( return OnBoardingState(
isDebugBuild = buildMeta.buildType != BuildType.RELEASE,
productionApplicationName = buildMeta.productionApplicationName, productionApplicationName = buildMeta.productionApplicationName,
canLoginWithQrCode = canLoginWithQrCode, canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = OnBoardingConfig.CAN_CREATE_ACCOUNT, canCreateAccount = OnBoardingConfig.CAN_CREATE_ACCOUNT,

View file

@ -8,7 +8,6 @@
package io.element.android.features.onboarding.impl package io.element.android.features.onboarding.impl
data class OnBoardingState( data class OnBoardingState(
val isDebugBuild: Boolean,
val productionApplicationName: String, val productionApplicationName: String,
val canLoginWithQrCode: Boolean, val canLoginWithQrCode: Boolean,
val canCreateAccount: Boolean, val canCreateAccount: Boolean,

View file

@ -16,17 +16,14 @@ open class OnBoardingStateProvider : PreviewParameterProvider<OnBoardingState> {
anOnBoardingState(canLoginWithQrCode = true), anOnBoardingState(canLoginWithQrCode = true),
anOnBoardingState(canCreateAccount = true), anOnBoardingState(canCreateAccount = true),
anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true), anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true),
anOnBoardingState(isDebugBuild = true),
) )
} }
fun anOnBoardingState( fun anOnBoardingState(
isDebugBuild: Boolean = false,
productionApplicationName: String = "Element", productionApplicationName: String = "Element",
canLoginWithQrCode: Boolean = false, canLoginWithQrCode: Boolean = false,
canCreateAccount: Boolean = false canCreateAccount: Boolean = false
) = OnBoardingState( ) = OnBoardingState(
isDebugBuild = isDebugBuild,
productionApplicationName = productionApplicationName, productionApplicationName = productionApplicationName,
canLoginWithQrCode = canLoginWithQrCode, canLoginWithQrCode = canLoginWithQrCode,
canCreateAccount = canCreateAccount canCreateAccount = canCreateAccount

View file

@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCode import androidx.compose.material.icons.filled.QrCode
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.BiasAlignment import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -28,7 +27,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom
import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
@ -36,8 +34,6 @@ import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage
import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.IconSource import io.element.android.libraries.designsystem.theme.components.IconSource
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextButton
@ -56,17 +52,13 @@ fun OnBoardingView(
onSignInWithQrCode: () -> Unit, onSignInWithQrCode: () -> Unit,
onSignIn: () -> Unit, onSignIn: () -> Unit,
onCreateAccount: () -> Unit, onCreateAccount: () -> Unit,
onOpenDeveloperSettings: () -> Unit,
onReportProblem: () -> Unit, onReportProblem: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
OnBoardingPage( OnBoardingPage(
modifier = modifier, modifier = modifier,
content = { content = {
OnBoardingContent( OnBoardingContent(state = state)
state = state,
onOpenDeveloperSettings = onOpenDeveloperSettings
)
}, },
footer = { footer = {
OnBoardingButtons( OnBoardingButtons(
@ -81,10 +73,7 @@ fun OnBoardingView(
} }
@Composable @Composable
private fun OnBoardingContent( private fun OnBoardingContent(state: OnBoardingState) {
state: OnBoardingState,
onOpenDeveloperSettings: () -> Unit,
) {
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
@ -127,17 +116,6 @@ private fun OnBoardingContent(
) )
} }
} }
if (state.isDebugBuild) {
IconButton(
modifier = Modifier.align(Alignment.TopEnd),
onClick = onOpenDeveloperSettings,
) {
Icon(
imageVector = CompoundIcons.SettingsSolid(),
contentDescription = stringResource(CommonStrings.common_settings)
)
}
}
} }
} }
@ -167,8 +145,8 @@ private fun OnBoardingButtons(
text = stringResource(id = signInButtonStringRes), text = stringResource(id = signInButtonStringRes),
onClick = onSignIn, onClick = onSignIn,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.testTag(TestTags.onBoardingSignIn) .testTag(TestTags.onBoardingSignIn)
) )
if (state.canCreateAccount) { if (state.canCreateAccount) {
TextButton( TextButton(
@ -181,8 +159,8 @@ private fun OnBoardingButtons(
// Add a report problem text button. Use a Text since we need a special theme here. // Add a report problem text button. Use a Text since we need a special theme here.
Text( Text(
modifier = Modifier modifier = Modifier
.padding(16.dp) .padding(16.dp)
.clickable(onClick = onReportProblem), .clickable(onClick = onReportProblem),
text = stringResource(id = CommonStrings.common_report_a_problem), text = stringResource(id = CommonStrings.common_report_a_problem),
style = ElementTheme.typography.fontBodySmRegular, style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary, color = ElementTheme.colors.textSecondary,
@ -200,7 +178,6 @@ internal fun OnBoardingViewPreview(
onSignInWithQrCode = {}, onSignInWithQrCode = {},
onSignIn = {}, onSignIn = {},
onCreateAccount = {}, onCreateAccount = {},
onOpenDeveloperSettings = {},
onReportProblem = {}, onReportProblem = {},
) )
} }

View file

@ -12,7 +12,6 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.OnBoardingConfig import io.element.android.appconfig.OnBoardingConfig
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.core.aBuildMeta
@ -44,27 +43,10 @@ class OnBoardingPresenterTest {
presenter.present() presenter.present()
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.isDebugBuild).isTrue()
assertThat(initialState.canLoginWithQrCode).isFalse() assertThat(initialState.canLoginWithQrCode).isFalse()
assertThat(initialState.productionApplicationName).isEqualTo("B") assertThat(initialState.productionApplicationName).isEqualTo("B")
assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT) assertThat(initialState.canCreateAccount).isEqualTo(OnBoardingConfig.CAN_CREATE_ACCOUNT)
assertThat(awaitItem().canLoginWithQrCode).isTrue() assertThat(awaitItem().canLoginWithQrCode).isTrue()
} }
} }
@Test
fun `present - initial state release`() = runTest {
val presenter = OnBoardingPresenter(
buildMeta = aBuildMeta(buildType = BuildType.RELEASE),
featureFlagService = FakeFeatureFlagService(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.isDebugBuild).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
} }

View file

@ -8,10 +8,8 @@
package io.element.android.features.onboarding.impl package io.element.android.features.onboarding.impl
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.ui.test.hasContentDescription
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
@ -74,17 +72,6 @@ class OnboardingViewTest {
} }
} }
@Test
fun `when on debug build - clicking on the settings icon opens the developer settings`() {
ensureCalledOnce { callback ->
rule.setOnboardingView(
state = anOnBoardingState(isDebugBuild = true),
onOpenDeveloperSettings = callback
)
rule.onNode(hasContentDescription(rule.activity.getString(CommonStrings.common_settings))).performClick()
}
}
@Test @Test
fun `clicking on report a problem calls the sign in callback`() { fun `clicking on report a problem calls the sign in callback`() {
ensureCalledOnce { callback -> ensureCalledOnce { callback ->
@ -101,7 +88,6 @@ class OnboardingViewTest {
onSignInWithQrCode: () -> Unit = EnsureNeverCalled(), onSignInWithQrCode: () -> Unit = EnsureNeverCalled(),
onSignIn: () -> Unit = EnsureNeverCalled(), onSignIn: () -> Unit = EnsureNeverCalled(),
onCreateAccount: () -> Unit = EnsureNeverCalled(), onCreateAccount: () -> Unit = EnsureNeverCalled(),
onOpenDeveloperSettings: () -> Unit = EnsureNeverCalled(),
onReportProblem: () -> Unit = EnsureNeverCalled(), onReportProblem: () -> Unit = EnsureNeverCalled(),
) { ) {
setContent { setContent {
@ -110,7 +96,6 @@ class OnboardingViewTest {
onSignInWithQrCode = onSignInWithQrCode, onSignInWithQrCode = onSignInWithQrCode,
onSignIn = onSignIn, onSignIn = onSignIn,
onCreateAccount = onCreateAccount, onCreateAccount = onCreateAccount,
onOpenDeveloperSettings = onOpenDeveloperSettings,
onReportProblem = onReportProblem, onReportProblem = onReportProblem,
) )
} }

View file

@ -1,12 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.api
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
interface ConfigureTracingEntryPoint : SimpleFeatureEntryPoint

View file

@ -1,24 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.preferences.api.ConfigureTracingEntryPoint
import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultConfigureTracingEntryPoint @Inject constructor() : ConfigureTracingEntryPoint {
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
return parentNode.createNode<ConfigureTracingNode>(buildContext)
}
}

View file

@ -30,7 +30,6 @@ import io.element.android.features.preferences.impl.advanced.AdvancedSettingsNod
import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode import io.element.android.features.preferences.impl.analytics.AnalyticsSettingsNode
import io.element.android.features.preferences.impl.blockedusers.BlockedUsersNode import io.element.android.features.preferences.impl.blockedusers.BlockedUsersNode
import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode import io.element.android.features.preferences.impl.developer.DeveloperSettingsNode
import io.element.android.features.preferences.impl.developer.tracing.ConfigureTracingNode
import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode import io.element.android.features.preferences.impl.notifications.NotificationSettingsNode
import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode import io.element.android.features.preferences.impl.notifications.edit.EditDefaultNotificationSettingNode
import io.element.android.features.preferences.impl.root.PreferencesRootNode import io.element.android.features.preferences.impl.root.PreferencesRootNode
@ -72,9 +71,6 @@ class PreferencesFlowNode @AssistedInject constructor(
@Parcelize @Parcelize
data object AdvancedSettings : NavTarget data object AdvancedSettings : NavTarget
@Parcelize
data object ConfigureTracing : NavTarget
@Parcelize @Parcelize
data object AnalyticsSettings : NavTarget data object AnalyticsSettings : NavTarget
@ -164,15 +160,7 @@ class PreferencesFlowNode @AssistedInject constructor(
createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback)) createNode<PreferencesRootNode>(buildContext, plugins = listOf(callback))
} }
NavTarget.DeveloperSettings -> { NavTarget.DeveloperSettings -> {
val callback = object : DeveloperSettingsNode.Callback { createNode<DeveloperSettingsNode>(buildContext)
override fun openConfigureTracing() {
backstack.push(NavTarget.ConfigureTracing)
}
}
createNode<DeveloperSettingsNode>(buildContext, listOf(callback))
}
NavTarget.ConfigureTracing -> {
createNode<ConfigureTracingNode>(buildContext)
} }
NavTarget.About -> { NavTarget.About -> {
val callback = object : AboutNode.Callback { val callback = object : AboutNode.Callback {

View file

@ -7,6 +7,7 @@
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
sealed interface DeveloperSettingsEvents { sealed interface DeveloperSettingsEvents {
@ -14,5 +15,6 @@ sealed interface DeveloperSettingsEvents {
data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents
data class SetSimplifiedSlidingSyncEnabled(val isEnabled: Boolean) : DeveloperSettingsEvents data class SetSimplifiedSlidingSyncEnabled(val isEnabled: Boolean) : DeveloperSettingsEvents
data class SetHideImagesAndVideos(val value: Boolean) : DeveloperSettingsEvents data class SetHideImagesAndVideos(val value: Boolean) : DeveloperSettingsEvents
data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents
data object ClearCache : DeveloperSettingsEvents data object ClearCache : DeveloperSettingsEvents
} }

View file

@ -15,7 +15,6 @@ import com.airbnb.android.showkase.models.Showkase
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
@ -28,14 +27,6 @@ class DeveloperSettingsNode @AssistedInject constructor(
@Assisted plugins: List<Plugin>, @Assisted plugins: List<Plugin>,
private val presenter: DeveloperSettingsPresenter, private val presenter: DeveloperSettingsPresenter,
) : Node(buildContext, plugins = plugins) { ) : Node(buildContext, plugins = plugins) {
interface Callback : Plugin {
fun openConfigureTracing()
}
private fun onOpenConfigureTracing() {
plugins<Callback>().forEach { it.openConfigureTracing() }
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity val activity = LocalContext.current as Activity
@ -49,7 +40,6 @@ class DeveloperSettingsNode @AssistedInject constructor(
state = state, state = state,
modifier = modifier, modifier = modifier,
onOpenShowkase = ::openShowkase, onOpenShowkase = ::openShowkase,
onOpenConfigureTracing = ::onOpenConfigureTracing,
onBackClick = ::navigateUp onBackClick = ::navigateUp
) )
} }

View file

@ -20,6 +20,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import io.element.android.appconfig.ElementCallConfig import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.logout.api.LogoutUseCase import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.features.preferences.impl.developer.tracing.toLogLevel
import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem
import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.ComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
@ -37,6 +39,7 @@ import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URL import java.net.URL
import javax.inject.Inject import javax.inject.Inject
@ -76,6 +79,11 @@ class DeveloperSettingsPresenter @Inject constructor(
.doesHideImagesAndVideosFlow() .doesHideImagesAndVideosFlow()
.collectAsState(initial = false) .collectAsState(initial = false)
val tracingLogLevel by appPreferencesStore
.getTracingLogLevelFlow()
.map { AsyncData.Success(it.toLogLevelItem()) }
.collectAsState(initial = AsyncData.Uninitialized)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
FeatureFlags.entries FeatureFlags.entries
.filter { it.isFinished.not() } .filter { it.isFinished.not() }
@ -123,6 +131,9 @@ class DeveloperSettingsPresenter @Inject constructor(
is DeveloperSettingsEvents.SetHideImagesAndVideos -> coroutineScope.launch { is DeveloperSettingsEvents.SetHideImagesAndVideos -> coroutineScope.launch {
appPreferencesStore.setHideImagesAndVideos(event.value) appPreferencesStore.setHideImagesAndVideos(event.value)
} }
is DeveloperSettingsEvents.SetTracingLogLevel -> coroutineScope.launch {
appPreferencesStore.setTracingLogLevel(event.logLevel.toLogLevel())
}
} }
} }
@ -138,6 +149,7 @@ class DeveloperSettingsPresenter @Inject constructor(
), ),
isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled, isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled,
hideImagesAndVideos = hideImagesAndVideos, hideImagesAndVideos = hideImagesAndVideos,
tracingLogLevel = tracingLogLevel,
eventSink = ::handleEvents eventSink = ::handleEvents
) )
} }

View file

@ -7,6 +7,7 @@
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState import io.element.android.features.rageshake.api.preferences.RageshakePreferencesState
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -21,6 +22,7 @@ data class DeveloperSettingsState(
val customElementCallBaseUrlState: CustomElementCallBaseUrlState, val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val isSimpleSlidingSyncEnabled: Boolean, val isSimpleSlidingSyncEnabled: Boolean,
val hideImagesAndVideos: Boolean, val hideImagesAndVideos: Boolean,
val tracingLogLevel: AsyncData<LogLevelItem>,
val eventSink: (DeveloperSettingsEvents) -> Unit val eventSink: (DeveloperSettingsEvents) -> Unit
) )

View file

@ -8,6 +8,7 @@
package io.element.android.features.preferences.impl.developer package io.element.android.features.preferences.impl.developer
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
@ -42,6 +43,7 @@ fun aDeveloperSettingsState(
customElementCallBaseUrlState = customElementCallBaseUrlState, customElementCallBaseUrlState = customElementCallBaseUrlState,
isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled, isSimpleSlidingSyncEnabled = isSimplifiedSlidingSyncEnabled,
hideImagesAndVideos = hideImagesAndVideos, hideImagesAndVideos = hideImagesAndVideos,
tracingLogLevel = AsyncData.Success(LogLevelItem.INFO),
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -14,8 +14,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView import io.element.android.features.rageshake.api.preferences.RageshakePreferencesView
import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory
import io.element.android.libraries.designsystem.components.preferences.PreferenceDropdown
import io.element.android.libraries.designsystem.components.preferences.PreferencePage import io.element.android.libraries.designsystem.components.preferences.PreferencePage
import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch import io.element.android.libraries.designsystem.components.preferences.PreferenceSwitch
import io.element.android.libraries.designsystem.components.preferences.PreferenceText import io.element.android.libraries.designsystem.components.preferences.PreferenceText
@ -25,12 +27,12 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.featureflag.ui.FeatureListView import io.element.android.libraries.featureflag.ui.FeatureListView
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.featureflag.ui.model.FeatureUiModel
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.toPersistentList
@Composable @Composable
fun DeveloperSettingsView( fun DeveloperSettingsView(
state: DeveloperSettingsState, state: DeveloperSettingsState,
onOpenShowkase: () -> Unit, onOpenShowkase: () -> Unit,
onOpenConfigureTracing: () -> Unit,
onBackClick: () -> Unit, onBackClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -49,9 +51,14 @@ fun DeveloperSettingsView(
} }
ElementCallCategory(state = state) ElementCallCategory(state = state)
PreferenceCategory(title = "Rust SDK") { PreferenceCategory(title = "Rust SDK") {
PreferenceText( PreferenceDropdown(
title = "Configure tracing", title = "Tracing log level",
onClick = onOpenConfigureTracing, supportingText = "Requires app reboot",
selectedOption = state.tracingLogLevel.dataOrNull(),
options = LogLevelItem.entries.toPersistentList(),
onSelectOption = { logLevel ->
state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(logLevel))
}
) )
PreferenceSwitch( PreferenceSwitch(
title = "Enable Simplified Sliding Sync", title = "Enable Simplified Sliding Sync",
@ -157,7 +164,6 @@ internal fun DeveloperSettingsViewPreview(@PreviewParameter(DeveloperSettingsSta
DeveloperSettingsView( DeveloperSettingsView(
state = state, state = state,
onOpenShowkase = {}, onOpenShowkase = {},
onOpenConfigureTracing = {},
onBackClick = {} onBackClick = {}
) )
} }

View file

@ -1,16 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
sealed interface ConfigureTracingEvents {
data class UpdateFilter(val target: Target, val logLevel: LogLevel) : ConfigureTracingEvents
data object ResetFilters : ConfigureTracingEvents
}

View file

@ -1,35 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.di.AppScope
@ContributesNode(AppScope::class)
class ConfigureTracingNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
private val presenter: ConfigureTracingPresenter,
) : Node(buildContext, plugins = plugins) {
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
ConfigureTracingView(
state = state,
onBackClick = ::navigateUp,
modifier = modifier
)
}
}

View file

@ -1,44 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import io.element.android.libraries.architecture.Presenter
import kotlinx.collections.immutable.toImmutableMap
import javax.inject.Inject
class ConfigureTracingPresenter @Inject constructor(
private val tracingConfigurationStore: TracingConfigurationStore,
private val targetLogLevelMapBuilder: TargetLogLevelMapBuilder,
) : Presenter<ConfigureTracingState> {
@Composable
override fun present(): ConfigureTracingState {
val modifiedMap = remember { mutableStateOf(targetLogLevelMapBuilder.getCurrentMap()) }
fun handleEvents(event: ConfigureTracingEvents) {
when (event) {
is ConfigureTracingEvents.UpdateFilter -> {
modifiedMap.value = modifiedMap.value.toMutableMap()
.apply { this[event.target] = event.logLevel }
tracingConfigurationStore.storeLogLevel(event.target, event.logLevel)
}
ConfigureTracingEvents.ResetFilters -> {
modifiedMap.value = targetLogLevelMapBuilder.getDefaultMap()
tracingConfigurationStore.reset()
}
}
}
return ConfigureTracingState(
targetsToLogLevel = modifiedMap.value.toImmutableMap(),
eventSink = ::handleEvents
)
}
}

View file

@ -1,17 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
import kotlinx.collections.immutable.ImmutableMap
data class ConfigureTracingState(
val targetsToLogLevel: ImmutableMap<Target, LogLevel>,
val eventSink: (ConfigureTracingEvents) -> Unit
)

View file

@ -1,29 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
import kotlinx.collections.immutable.persistentMapOf
open class ConfigureTracingStateProvider : PreviewParameterProvider<ConfigureTracingState> {
override val values: Sequence<ConfigureTracingState>
get() = sequenceOf(
aConfigureTracingState(),
)
}
fun aConfigureTracingState() = ConfigureTracingState(
targetsToLogLevel = persistentMapOf(
Target.ELEMENT to LogLevel.INFO,
Target.MATRIX_SDK_FFI to LogLevel.WARN,
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.ERROR,
),
eventSink = {}
)

View file

@ -1,234 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
import kotlinx.collections.immutable.ImmutableMap
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ConfigureTracingView(
state: ConfigureTracingState,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
var showMenu by remember { mutableStateOf(false) }
Scaffold(
modifier = modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding(),
contentWindowInsets = WindowInsets.statusBars,
topBar = {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackClick)
},
title = {
Text(
text = "Configure tracing",
style = ElementTheme.typography.aliasScreenTitle,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
actions = {
IconButton(
onClick = { showMenu = !showMenu }
) {
Icon(
imageVector = CompoundIcons.OverflowVertical(),
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
onClick = {
showMenu = false
state.eventSink.invoke(ConfigureTracingEvents.ResetFilters)
},
text = { Text("Reset to default") },
leadingIcon = {
Icon(
imageVector = CompoundIcons.Delete(),
tint = ElementTheme.materialColors.secondary,
contentDescription = null,
)
}
)
}
}
)
},
content = {
Column(
modifier = Modifier
.padding(it)
.consumeWindowInsets(it)
.verticalScroll(state = rememberScrollState())
) {
ListItem(
headlineContent = {
Text(
modifier = Modifier.clickable { Runtime.getRuntime().exit(0) },
text = "Tap here to kill the app and apply the changes. You'll have to re-open the app manually.",
style = ElementTheme.typography.fontHeadingSmMedium,
)
},
)
HorizontalDivider()
CrateListContent(state)
}
}
)
}
@Composable
private fun CrateListContent(
state: ConfigureTracingState,
) {
fun onLogLevelChange(target: Target, logLevel: LogLevel) {
state.eventSink(ConfigureTracingEvents.UpdateFilter(target, logLevel))
}
TargetAndLogLevelListView(
data = state.targetsToLogLevel,
onLogLevelChange = ::onLogLevelChange,
)
}
@Composable
private fun TargetAndLogLevelListView(
data: ImmutableMap<Target, LogLevel>,
onLogLevelChange: (Target, LogLevel) -> Unit,
) {
Column {
data.forEach { item ->
fun onLogLevelChange(logLevel: LogLevel) {
onLogLevelChange(item.key, logLevel)
}
TargetAndLogLevelView(
target = item.key,
logLevel = item.value,
onLogLevelChange = ::onLogLevelChange
)
}
}
}
@Composable
private fun TargetAndLogLevelView(
target: Target,
logLevel: LogLevel,
onLogLevelChange: (LogLevel) -> Unit,
) {
ListItem(
headlineContent = { Text(text = target.filter.takeIf { it.isNotEmpty() } ?: "(common)") },
trailingContent = ListItemContent.Custom {
LogLevelDropdownMenu(
logLevel = logLevel,
onLogLevelChange = onLogLevelChange,
)
},
)
}
@Composable
private fun LogLevelDropdownMenu(
logLevel: LogLevel,
onLogLevelChange: (LogLevel) -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
Box {
DropdownMenuItem(
modifier = Modifier.widthIn(max = 120.dp),
text = { Text(text = logLevel.filter) },
onClick = { expanded = !expanded },
trailingIcon = {
Icon(
modifier = Modifier.rotate(if (expanded) 180f else 0f),
imageVector = CompoundIcons.ChevronDown(),
contentDescription = null,
)
},
)
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
LogLevel.values().forEach { logLevel ->
DropdownMenuItem(
text = {
Text(text = logLevel.filter)
},
onClick = {
expanded = false
onLogLevelChange(logLevel)
}
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun ConfigureTracingViewPreview(
@PreviewParameter(ConfigureTracingStateProvider::class) state: ConfigureTracingState
) = ElementPreview {
ConfigureTracingView(
state = state,
onBackClick = {},
)
}

View file

@ -0,0 +1,28 @@
/*
* 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.preferences.impl.developer.tracing
import io.element.android.libraries.designsystem.components.preferences.DropdownOption
enum class LogLevelItem : DropdownOption {
ERROR {
override val text: String = "Error"
},
WARN {
override val text: String = "Warn"
},
INFO {
override val text: String = "Info"
},
DEBUG {
override val text: String = "Debug"
},
TRACE {
override val text: String = "Trace"
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.preferences.impl.developer.tracing
import io.element.android.libraries.matrix.api.tracing.LogLevel
fun LogLevelItem.toLogLevel(): LogLevel {
return when (this) {
LogLevelItem.ERROR -> io.element.android.libraries.matrix.api.tracing.LogLevel.ERROR
LogLevelItem.WARN -> io.element.android.libraries.matrix.api.tracing.LogLevel.WARN
LogLevelItem.INFO -> io.element.android.libraries.matrix.api.tracing.LogLevel.INFO
LogLevelItem.DEBUG -> io.element.android.libraries.matrix.api.tracing.LogLevel.DEBUG
LogLevelItem.TRACE -> io.element.android.libraries.matrix.api.tracing.LogLevel.TRACE
}
}
fun LogLevel.toLogLevelItem(): LogLevelItem {
return when (this) {
LogLevel.ERROR -> LogLevelItem.ERROR
LogLevel.WARN -> LogLevelItem.WARN
LogLevel.INFO -> LogLevelItem.INFO
LogLevel.DEBUG -> LogLevelItem.DEBUG
LogLevel.TRACE -> LogLevelItem.TRACE
}
}

View file

@ -1,31 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfiguration
import javax.inject.Inject
class TargetLogLevelMapBuilder @Inject constructor(
private val tracingConfigurationStore: TracingConfigurationStore,
private val defaultConfig: TracingFilterConfiguration,
) {
fun getDefaultMap(): Map<Target, LogLevel> {
return Target.entries.associateWith { target ->
defaultConfig.getLogLevel(target)
}
}
fun getCurrentMap(): Map<Target, LogLevel> {
return Target.entries.associateWith { target ->
tracingConfigurationStore.getLogLevel(target)
?: defaultConfig.getLogLevel(target)
}
}
}

View file

@ -1,50 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import android.content.SharedPreferences
import androidx.core.content.edit
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
import javax.inject.Inject
interface TracingConfigurationStore {
fun getLogLevel(target: Target): LogLevel?
fun storeLogLevel(target: Target, logLevel: LogLevel)
fun reset()
}
@ContributesBinding(AppScope::class)
class SharedPreferencesTracingConfigurationStore @Inject constructor(
private val sharedPreferences: SharedPreferences
) : TracingConfigurationStore {
override fun getLogLevel(target: Target): LogLevel? {
return sharedPreferences.getString("$KEY_PREFIX${target.name}", null)
?.let { LogLevel.valueOf(it) }
}
override fun storeLogLevel(target: Target, logLevel: LogLevel) {
sharedPreferences.edit {
putString("$KEY_PREFIX${target.name}", logLevel.name)
}
}
override fun reset() {
sharedPreferences.edit {
sharedPreferences.all.keys.filter { it.startsWith(KEY_PREFIX) }.forEach {
remove(it)
}
}
}
companion object {
private const val KEY_PREFIX = "tracing_log_level_"
}
}

View file

@ -12,6 +12,7 @@ package io.element.android.features.preferences.impl.developer
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.ElementCallConfig import io.element.android.appconfig.ElementCallConfig
import io.element.android.features.logout.test.FakeLogoutUseCase import io.element.android.features.logout.test.FakeLogoutUseCase
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase
import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase
import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState
@ -52,11 +53,13 @@ class DeveloperSettingsPresenterTest {
assertThat(state.rageshakeState.isEnabled).isFalse() assertThat(state.rageshakeState.isEnabled).isFalse()
assertThat(state.rageshakeState.isSupported).isTrue() assertThat(state.rageshakeState.isSupported).isTrue()
assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f) assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f)
assertThat(state.tracingLogLevel).isEqualTo(AsyncData.Uninitialized)
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.features).isNotEmpty() assertThat(state.features).isNotEmpty()
val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() } val numberOfModifiableFeatureFlags = FeatureFlags.entries.count { it.isFinished.not() }
assertThat(state.features).hasSize(numberOfModifiableFeatureFlags) assertThat(state.features).hasSize(numberOfModifiableFeatureFlags)
assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO)
} }
awaitItem().also { state -> awaitItem().also { state ->
assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java) assertThat(state.cacheSize).isInstanceOf(AsyncData.Success::class.java)
@ -200,6 +203,22 @@ class DeveloperSettingsPresenterTest {
} }
} }
@Test
fun `present - changing tracing log level`() = runTest {
val preferences = InMemoryAppPreferencesStore()
val presenter = createDeveloperSettingsPresenter(preferencesStore = preferences)
presenter.test {
skipItems(2)
awaitItem().also { state ->
assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.INFO)
state.eventSink(DeveloperSettingsEvents.SetTracingLogLevel(LogLevelItem.TRACE))
}
awaitItem().also { state ->
assertThat(state.tracingLogLevel.dataOrNull()).isEqualTo(LogLevelItem.TRACE)
}
}
}
private fun createDeveloperSettingsPresenter( private fun createDeveloperSettingsPresenter(
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(), featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(),
cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(), cacheSizeUseCase: FakeComputeCacheSizeUseCase = FakeComputeCacheSizeUseCase(),

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.preferences.impl.R import io.element.android.features.preferences.impl.R
import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EventsRecorder import io.element.android.tests.testutils.EventsRecorder
@ -76,17 +77,16 @@ class DeveloperSettingsViewTest {
@Config(qualifiers = "h1024dp") @Config(qualifiers = "h1024dp")
@Test @Test
fun `clicking on configure tracing invokes the expected callback`() { fun `clicking on log level emits the expected event`() {
val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>(expectEvents = false) val eventsRecorder = EventsRecorder<DeveloperSettingsEvents>()
ensureCalledOnce { rule.setDeveloperSettingsView(
rule.setDeveloperSettingsView( state = aDeveloperSettingsState(
state = aDeveloperSettingsState( eventSink = eventsRecorder
eventSink = eventsRecorder ),
), )
onOpenConfigureTracing = it rule.onNodeWithText("Tracing log level").performClick()
) rule.onNodeWithText("Debug").performClick()
rule.onNodeWithText("Configure tracing").performClick() eventsRecorder.assertSingle(DeveloperSettingsEvents.SetTracingLogLevel(LogLevelItem.DEBUG))
}
} }
@Config(qualifiers = "h1500dp") @Config(qualifiers = "h1500dp")
@ -131,14 +131,12 @@ class DeveloperSettingsViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeveloperSettingsView(
state: DeveloperSettingsState, state: DeveloperSettingsState,
onOpenShowkase: () -> Unit = EnsureNeverCalled(), onOpenShowkase: () -> Unit = EnsureNeverCalled(),
onOpenConfigureTracing: () -> Unit = EnsureNeverCalled(),
onBackClick: () -> Unit = EnsureNeverCalled() onBackClick: () -> Unit = EnsureNeverCalled()
) { ) {
setContent { setContent {
DeveloperSettingsView( DeveloperSettingsView(
state = state, state = state,
onOpenShowkase = onOpenShowkase, onOpenShowkase = onOpenShowkase,
onOpenConfigureTracing = onOpenConfigureTracing,
onBackClick = onBackClick, onBackClick = onBackClick,
) )
} }

View file

@ -1,101 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.waitForPredicate
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
class ConfigureTracingPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - initial state`() = runTest {
val store = InMemoryTracingConfigurationStore()
val presenter = ConfigureTracingPresenter(
store,
TargetLogLevelMapBuilder(store, TracingFilterConfigurations.debug),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.targetsToLogLevel).isNotEmpty()
assertThat(initialState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.DEBUG)
}
}
@Test
fun `present - store is taken into account`() = runTest {
val store = InMemoryTracingConfigurationStore()
store.givenLogLevel(LogLevel.ERROR)
val presenter = ConfigureTracingPresenter(
store,
TargetLogLevelMapBuilder(store, TracingFilterConfigurations.debug),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.targetsToLogLevel).isNotEmpty()
assertThat(initialState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.ERROR)
}
}
@Test
fun `present - change a value`() = runTest {
val store = InMemoryTracingConfigurationStore()
val presenter = ConfigureTracingPresenter(
store,
TargetLogLevelMapBuilder(store, TracingFilterConfigurations.debug),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.DEBUG)
initialState.eventSink.invoke(ConfigureTracingEvents.UpdateFilter(Target.MATRIX_SDK_CRYPTO, LogLevel.WARN))
val finalState = awaitItem()
assertThat(finalState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.WARN)
waitForPredicate { store.hasStoreLogLevelBeenCalled }
}
}
@Test
fun `present - reset`() = runTest {
val store = InMemoryTracingConfigurationStore()
val presenter = ConfigureTracingPresenter(
store,
TargetLogLevelMapBuilder(store, TracingFilterConfigurations.debug),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.DEBUG)
initialState.eventSink.invoke(ConfigureTracingEvents.UpdateFilter(Target.MATRIX_SDK_CRYPTO, LogLevel.WARN))
val finalState = awaitItem()
assertThat(finalState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.WARN)
waitForPredicate { store.hasStoreLogLevelBeenCalled }
finalState.eventSink.invoke(ConfigureTracingEvents.ResetFilters)
val resetState = awaitItem()
assertThat(resetState.targetsToLogLevel[Target.MATRIX_SDK_CRYPTO]).isEqualTo(LogLevel.DEBUG)
waitForPredicate { store.hasResetBeenCalled }
}
}
}

View file

@ -1,35 +0,0 @@
/*
* 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.
*/
package io.element.android.features.preferences.impl.developer.tracing
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.Target
class InMemoryTracingConfigurationStore : TracingConfigurationStore {
var hasResetBeenCalled = false
private set
var hasStoreLogLevelBeenCalled = false
private set
private var logLevel: LogLevel? = null
fun givenLogLevel(logLevel: LogLevel?) {
this.logLevel = logLevel
}
override fun getLogLevel(target: Target): LogLevel? {
return logLevel
}
override fun storeLogLevel(target: Target, logLevel: LogLevel) {
hasStoreLogLevelBeenCalled = true
}
override fun reset() {
hasResetBeenCalled = true
}
}

View file

@ -35,9 +35,9 @@ interface BugReporter {
fun logDirectory(): File fun logDirectory(): File
/** /**
* Set the current tracing filter. * Set the current tracing log level.
*/ */
fun setCurrentTracingFilter(tracingFilter: String) fun setCurrentTracingLogLevel(logLevel: String)
/** /**
* Save the logcat. * Save the logcat.

View file

@ -79,7 +79,7 @@ class DefaultBugReporter @Inject constructor(
} }
private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*") private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
private var currentTracingFilter: String? = null private var currentTracingLogLevel: String? = null
private val logCatErrFile = File(logDirectory().absolutePath, LOG_CAT_FILENAME) private val logCatErrFile = File(logDirectory().absolutePath, LOG_CAT_FILENAME)
@ -156,8 +156,8 @@ class DefaultBugReporter @Inject constructor(
if (crashCallStack.isNotEmpty() && withCrashLogs) { if (crashCallStack.isNotEmpty() && withCrashLogs) {
builder.addFormDataPart("label", "crash") builder.addFormDataPart("label", "crash")
} }
currentTracingFilter?.let { currentTracingLogLevel?.let {
builder.addFormDataPart("tracing_filter", it) builder.addFormDataPart("tracing_log_level", it)
} }
if (buildMeta.isEnterpriseBuild) { if (buildMeta.isEnterpriseBuild) {
builder.addFormDataPart("label", "Enterprise") builder.addFormDataPart("label", "Enterprise")
@ -299,8 +299,8 @@ class DefaultBugReporter @Inject constructor(
} }
} }
override fun setCurrentTracingFilter(tracingFilter: String) { override fun setCurrentTracingLogLevel(logLevel: String) {
currentTracingFilter = tracingFilter currentTracingLogLevel = logLevel
} }
/** /**

View file

@ -53,7 +53,7 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter {
return File("fake") return File("fake")
} }
override fun setCurrentTracingFilter(tracingFilter: String) { override fun setCurrentTracingLogLevel(logLevel: String) {
// No op // No op
} }

View file

@ -173,7 +173,7 @@ jsoup = "org.jsoup:jsoup:1.18.3"
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
timber = "com.jakewharton.timber:timber:5.0.1" timber = "com.jakewharton.timber:timber:5.0.1"
matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.73" matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.75"
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }

View file

@ -0,0 +1,189 @@
/*
* 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.
*/
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.libraries.designsystem.components.preferences
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MenuAnchorType
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.components.list.ListItemContent
import io.element.android.libraries.designsystem.components.preferences.components.preferenceIcon
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
import io.element.android.libraries.designsystem.theme.components.ListItem
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.toEnabledColor
import io.element.android.libraries.designsystem.toSecondaryEnabledColor
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun <T : DropdownOption> PreferenceDropdown(
title: String,
selectedOption: T?,
options: ImmutableList<T>,
onSelectOption: (T) -> Unit,
modifier: Modifier = Modifier,
supportingText: String? = null,
enabled: Boolean = true,
icon: ImageVector? = null,
@DrawableRes iconResourceId: Int? = null,
showIconAreaIfNoIcon: Boolean = false,
) {
var isDropdownExpanded by remember { mutableStateOf(false) }
ListItem(
modifier = modifier,
leadingContent = preferenceIcon(
icon = icon,
iconResourceId = iconResourceId,
enabled = enabled,
showIconAreaIfNoIcon = showIconAreaIfNoIcon,
),
headlineContent = {
Text(
style = ElementTheme.typography.fontBodyLgRegular,
modifier = Modifier.fillMaxWidth(),
text = title,
color = enabled.toEnabledColor(),
)
},
supportingContent = supportingText?.let {
{
Text(
style = ElementTheme.typography.fontBodyMdRegular,
text = it,
color = enabled.toSecondaryEnabledColor(),
)
}
},
trailingContent = ListItemContent.Custom(
content = {
DropdownTrailingContent(
selectedOption = selectedOption,
options = options,
onSelectOption = onSelectOption,
expanded = isDropdownExpanded,
onExpandedChange = { isDropdownExpanded = it },
)
}
),
onClick = { isDropdownExpanded = true }.takeIf { !isDropdownExpanded },
)
}
/**
* A dropdown option that can be used in a [PreferenceDropdown].
*/
interface DropdownOption {
/**
* The text to display for this option.
*/
val text: String
}
@Composable
private fun <T : DropdownOption> DropdownTrailingContent(
selectedOption: T?,
options: ImmutableList<T>,
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
onSelectOption: (T) -> Unit,
modifier: Modifier = Modifier,
) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = onExpandedChange,
modifier = modifier,
) {
Row(
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = selectedOption?.text.orEmpty(),
maxLines = 1,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textSecondary,
)
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
}
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { onExpandedChange(false) },
matchTextFieldWidth = false,
) {
options.forEach { option ->
DropdownMenuItem(
text = {
Text(
text = option.text,
style = ElementTheme.typography.fontBodyMdRegular
)
},
onClick = {
onSelectOption(option)
onExpandedChange(false)
},
)
}
}
}
}
@Preview(group = PreviewGroup.Preferences)
@Composable
internal fun PreferenceDropdownPreview() = ElementThemedPreview {
val options = listOf(
object : DropdownOption {
override val text = "Option 1"
},
object : DropdownOption {
override val text = "Option 2"
},
object : DropdownOption {
override val text = "Option 3"
},
).toImmutableList()
Column {
PreferenceDropdown(
title = "Dropdown",
supportingText = "Options for dropdown",
icon = CompoundIcons.Threads(),
selectedOption = null,
options = options,
onSelectOption = {},
)
PreferenceDropdown(
title = "Dropdown",
supportingText = "Options for dropdown",
icon = CompoundIcons.Threads(),
selectedOption = options.first(),
options = options,
onSelectOption = {},
)
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2022-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.
*/
package io.element.android.libraries.matrix.api.tracing
/**
* Log levels for tracing in the SDK.
*/
enum class LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
TRACE,
}

View file

@ -8,7 +8,8 @@
package io.element.android.libraries.matrix.api.tracing package io.element.android.libraries.matrix.api.tracing
data class TracingConfiguration( data class TracingConfiguration(
val filterConfiguration: TracingFilterConfiguration, val logLevel: LogLevel,
val extraTargets: List<String>,
val writesToLogcat: Boolean, val writesToLogcat: Boolean,
val writesToFilesConfiguration: WriteToFilesConfiguration, val writesToFilesConfiguration: WriteToFilesConfiguration,
) )

View file

@ -1,102 +0,0 @@
/*
* Copyright 2022-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.
*/
package io.element.android.libraries.matrix.api.tracing
data class TracingFilterConfiguration(
val overrides: Map<Target, LogLevel> = emptyMap(),
) {
private val defaultLogLevel = LogLevel.INFO
// Order should matters
private val targetsToLogLevel: Map<Target, LogLevel> = mapOf(
Target.HYPER to LogLevel.WARN,
Target.MATRIX_SDK_CRYPTO to LogLevel.DEBUG,
Target.MATRIX_SDK_CRYPTO_ACCOUNT to LogLevel.TRACE,
Target.MATRIX_SDK_HTTP_CLIENT to LogLevel.DEBUG,
Target.MATRIX_SDK_SLIDING_SYNC to LogLevel.INFO,
Target.MATRIX_SDK_BASE_SLIDING_SYNC to LogLevel.INFO,
Target.MATRIX_SDK_UI_TIMELINE to LogLevel.INFO,
Target.MATRIX_SDK_BASE_CLIENT to LogLevel.TRACE,
// To debug OIDC logouts
Target.MATRIX_SDK_OIDC to LogLevel.TRACE,
)
fun getLogLevel(target: Target): LogLevel {
return overrides[target] ?: targetsToLogLevel[target] ?: defaultLogLevel
}
val filter: String
get() {
val fullMap = Target.entries.associateWith {
overrides[it] ?: targetsToLogLevel[it] ?: defaultLogLevel
}
return fullMap.map {
if (it.key.filter.isEmpty()) {
it.value.filter
} else {
"${it.key.filter}=${it.value.filter}"
}
}.joinToString(separator = ",")
}
}
enum class Target(open val filter: String) {
// COMMON(""),
ELEMENT("elementx"),
HYPER("hyper"),
MATRIX_SDK_FFI("matrix_sdk_ffi"),
MATRIX_SDK_UNIFFI_API("matrix_sdk_ffi::uniffi_api"),
MATRIX_SDK_CRYPTO("matrix_sdk_crypto"),
MATRIX_SDK_CRYPTO_ACCOUNT("matrix_sdk_crypto::olm::account"),
MATRIX_SDK("matrix_sdk"),
MATRIX_SDK_HTTP_CLIENT("matrix_sdk::http_client"),
MATRIX_SDK_CLIENT("matrix_sdk::client"),
MATRIX_SDK_OIDC("matrix_sdk::oidc"),
MATRIX_SDK_SEND_QUEUE("matrix_sdk::send_queue"),
MATRIX_SDK_SLIDING_SYNC("matrix_sdk::sliding_sync"),
MATRIX_SDK_BASE_SLIDING_SYNC("matrix_sdk_base::sliding_sync"),
MATRIX_SDK_UI_TIMELINE("matrix_sdk_ui::timeline"),
MATRIX_SDK_BASE_READ_RECEIPTS("matrix_sdk_base::read_receipts"),
MATRIX_SDK_BASE_CLIENT("matrix_sdk_base"),
}
enum class LogLevel(open val filter: String) {
ERROR("error"),
WARN("warn"),
INFO("info"),
DEBUG("debug"),
TRACE("trace"),
}
object TracingFilterConfigurations {
val release = TracingFilterConfiguration(
overrides = mapOf(
Target.ELEMENT to LogLevel.DEBUG
),
)
val nightly = TracingFilterConfiguration(
overrides = mapOf(
Target.ELEMENT to LogLevel.TRACE,
),
)
val debug = TracingFilterConfiguration(
overrides = mapOf(
Target.ELEMENT to LogLevel.TRACE
)
)
/**
* Use this method to create a custom configuration where all targets will have the same log level.
*/
fun custom(logLevel: LogLevel) = TracingFilterConfiguration(overrides = Target.entries.associateWith { logLevel })
/**
* Use this method to override the log level of specific targets.
*/
fun custom(overrides: Map<Target, LogLevel>) = TracingFilterConfiguration(overrides)
}

View file

@ -11,5 +11,5 @@ import timber.log.Timber
interface TracingService { interface TracingService {
fun setupTracing(tracingConfiguration: TracingConfiguration) fun setupTracing(tracingConfiguration: TracingConfiguration)
fun createTimberTree(): Timber.Tree fun createTimberTree(target: String): Timber.Tree
} }

View file

@ -311,8 +311,8 @@ class RustMatrixClient(
isEncrypted = createRoomParams.isEncrypted, isEncrypted = createRoomParams.isEncrypted,
isDirect = createRoomParams.isDirect, isDirect = createRoomParams.isDirect,
visibility = when (createRoomParams.visibility) { visibility = when (createRoomParams.visibility) {
RoomVisibility.PUBLIC -> RustRoomVisibility.PUBLIC RoomVisibility.PUBLIC -> RustRoomVisibility.Public
RoomVisibility.PRIVATE -> RustRoomVisibility.PRIVATE RoomVisibility.PRIVATE -> RustRoomVisibility.Private
}, },
preset = when (createRoomParams.visibility) { preset = when (createRoomParams.visibility) {
RoomVisibility.PRIVATE -> { RoomVisibility.PRIVATE -> {

View file

@ -1,30 +0,0 @@
/*
* Copyright 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.
*/
package io.element.android.libraries.matrix.impl.di
import com.squareup.anvil.annotations.ContributesTo
import dagger.Module
import dagger.Provides
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.matrix.api.tracing.TracingFilterConfiguration
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
@Module
@ContributesTo(AppScope::class)
object TracingMatrixModule {
@Provides
fun providesTracingFilterConfiguration(buildMeta: BuildMeta): TracingFilterConfiguration {
return when (buildMeta.buildType) {
BuildType.DEBUG -> TracingFilterConfigurations.debug
BuildType.NIGHTLY -> TracingFilterConfigurations.nightly
BuildType.RELEASE -> TracingFilterConfigurations.release
}
}
}

View file

@ -28,4 +28,5 @@ fun ImageInfo.map(): RustImageInfo = RustImageInfo(
thumbnailInfo = thumbnailInfo?.map(), thumbnailInfo = thumbnailInfo?.map(),
thumbnailSource = null, thumbnailSource = null,
blurhash = blurhash, blurhash = blurhash,
isAnimated = null
) )

View file

@ -71,6 +71,7 @@ import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat import org.matrix.rustcomponents.sdk.MessageFormat
import org.matrix.rustcomponents.sdk.PollData import org.matrix.rustcomponents.sdk.PollData
import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle import org.matrix.rustcomponents.sdk.SendAttachmentJoinHandle
import org.matrix.rustcomponents.sdk.UploadParameters
import org.matrix.rustcomponents.sdk.use import org.matrix.rustcomponents.sdk.use
import timber.log.Timber import timber.log.Timber
import uniffi.matrix_sdk_ui.LiveBackPaginationStatus import uniffi.matrix_sdk_ui.LiveBackPaginationStatus
@ -323,6 +324,7 @@ class RustTimeline(
formattedCaption = formattedCaption?.let { formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html) FormattedBody(body = it, format = MessageFormat.Html)
}, },
mentions = null,
) )
inner.edit( inner.edit(
newContent = editedContent, newContent = editedContent,
@ -355,14 +357,17 @@ class RustTimeline(
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOfNotNull(file, thumbnailFile)) { return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendImage( inner.sendImage(
url = file.path, params = UploadParameters(
thumbnailUrl = thumbnailFile?.path, filename = file.path,
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
mentions = null,
),
thumbnailPath = thumbnailFile?.path,
imageInfo = imageInfo.map(), imageInfo = imageInfo.map(),
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
progressWatcher = progressCallback?.toProgressWatcher() progressWatcher = progressCallback?.toProgressWatcher()
) )
} }
@ -379,14 +384,17 @@ class RustTimeline(
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOfNotNull(file, thumbnailFile)) { return sendAttachment(listOfNotNull(file, thumbnailFile)) {
inner.sendVideo( inner.sendVideo(
url = file.path, params = UploadParameters(
thumbnailUrl = thumbnailFile?.path, filename = file.path,
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
mentions = null,
),
thumbnailPath = thumbnailFile?.path,
videoInfo = videoInfo.map(), videoInfo = videoInfo.map(),
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
progressWatcher = progressCallback?.toProgressWatcher() progressWatcher = progressCallback?.toProgressWatcher()
) )
} }
@ -402,13 +410,16 @@ class RustTimeline(
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOf(file)) { return sendAttachment(listOf(file)) {
inner.sendAudio( inner.sendAudio(
url = file.path, params = UploadParameters(
filename = file.path,
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
mentions = null,
),
audioInfo = audioInfo.map(), audioInfo = audioInfo.map(),
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
progressWatcher = progressCallback?.toProgressWatcher() progressWatcher = progressCallback?.toProgressWatcher()
) )
} }
@ -424,13 +435,16 @@ class RustTimeline(
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOf(file)) { return sendAttachment(listOf(file)) {
inner.sendFile( inner.sendFile(
url = file.path, params = UploadParameters(
filename = file.path,
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
mentions = null,
),
fileInfo = fileInfo.map(), fileInfo = fileInfo.map(),
caption = caption,
formattedCaption = formattedCaption?.let {
FormattedBody(body = it, format = MessageFormat.Html)
},
useSendQueue = useSendQueue,
progressWatcher = progressCallback?.toProgressWatcher(), progressWatcher = progressCallback?.toProgressWatcher(),
) )
} }
@ -543,13 +557,16 @@ class RustTimeline(
val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue) val useSendQueue = featureFlagsService.isFeatureEnabled(FeatureFlags.MediaUploadOnSendQueue)
return sendAttachment(listOf(file)) { return sendAttachment(listOf(file)) {
inner.sendVoiceMessage( inner.sendVoiceMessage(
url = file.path, params = UploadParameters(
filename = file.path,
// Maybe allow a caption in the future?
caption = null,
formattedCaption = null,
useSendQueue = useSendQueue,
mentions = null,
),
audioInfo = audioInfo.map(), audioInfo = audioInfo.map(),
waveform = waveform.toMSC3246range(), waveform = waveform.toMSC3246range(),
// Maybe allow a caption in the future?
caption = null,
formattedCaption = null,
useSendQueue = useSendQueue,
progressWatcher = progressCallback?.toProgressWatcher(), progressWatcher = progressCallback?.toProgressWatcher(),
) )
} }

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.tracing
import com.squareup.anvil.annotations.ContributesBinding import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
@ -20,18 +21,28 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : TracingService { class RustTracingService @Inject constructor(private val buildMeta: BuildMeta) : TracingService {
override fun setupTracing(tracingConfiguration: TracingConfiguration) { override fun setupTracing(tracingConfiguration: TracingConfiguration) {
val filter = tracingConfiguration.filterConfiguration
val rustTracingConfiguration = org.matrix.rustcomponents.sdk.TracingConfiguration( val rustTracingConfiguration = org.matrix.rustcomponents.sdk.TracingConfiguration(
filter = tracingConfiguration.filterConfiguration.filter,
writeToStdoutOrSystem = tracingConfiguration.writesToLogcat, writeToStdoutOrSystem = tracingConfiguration.writesToLogcat,
logLevel = tracingConfiguration.logLevel.toRustLogLevel(),
extraTargets = tracingConfiguration.extraTargets,
writeToFiles = tracingConfiguration.writesToFilesConfiguration.toTracingFileConfiguration(), writeToFiles = tracingConfiguration.writesToFilesConfiguration.toTracingFileConfiguration(),
) )
org.matrix.rustcomponents.sdk.setupTracing(rustTracingConfiguration) org.matrix.rustcomponents.sdk.setupTracing(rustTracingConfiguration)
Timber.v("Tracing config filter = $filter: ${filter.filter}") Timber.d("setupTracing: $rustTracingConfiguration")
} }
override fun createTimberTree(): Timber.Tree { override fun createTimberTree(target: String): Timber.Tree {
return RustTracingTree(retrieveFromStackTrace = buildMeta.isDebuggable) return RustTracingTree(target = target, retrieveFromStackTrace = buildMeta.isDebuggable)
}
}
private fun LogLevel.toRustLogLevel(): org.matrix.rustcomponents.sdk.LogLevel {
return when (this) {
LogLevel.ERROR -> org.matrix.rustcomponents.sdk.LogLevel.ERROR
LogLevel.WARN -> org.matrix.rustcomponents.sdk.LogLevel.WARN
LogLevel.INFO -> org.matrix.rustcomponents.sdk.LogLevel.INFO
LogLevel.DEBUG -> org.matrix.rustcomponents.sdk.LogLevel.DEBUG
LogLevel.TRACE -> org.matrix.rustcomponents.sdk.LogLevel.TRACE
} }
} }

View file

@ -8,7 +8,6 @@
package io.element.android.libraries.matrix.impl.tracing package io.element.android.libraries.matrix.impl.tracing
import android.util.Log import android.util.Log
import io.element.android.libraries.matrix.api.tracing.Target
import org.matrix.rustcomponents.sdk.LogLevel import org.matrix.rustcomponents.sdk.LogLevel
import org.matrix.rustcomponents.sdk.logEvent import org.matrix.rustcomponents.sdk.logEvent
import timber.log.Timber import timber.log.Timber
@ -26,7 +25,10 @@ private val fqcnIgnore = listOf(
/** /**
* A Timber tree that passes logs to the Rust SDK. * A Timber tree that passes logs to the Rust SDK.
*/ */
internal class RustTracingTree(private val retrieveFromStackTrace: Boolean) : Timber.Tree() { internal class RustTracingTree(
private val target: String,
private val retrieveFromStackTrace: Boolean,
) : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
val location = if (retrieveFromStackTrace) { val location = if (retrieveFromStackTrace) {
getLogEventLocationFromStackTrace() getLogEventLocationFromStackTrace()
@ -38,7 +40,7 @@ internal class RustTracingTree(private val retrieveFromStackTrace: Boolean) : Ti
file = location.file, file = location.file,
line = location.line, line = location.line,
level = logLevel, level = logLevel,
target = Target.ELEMENT.filter, target = target,
message = if (tag != null) "[$tag] $message" else message, message = if (tag != null) "[$tag] $message" else message,
) )
} }

View file

@ -1,34 +0,0 @@
/*
* Copyright 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.
*/
package io.element.android.libraries.matrix.impl.di
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
import io.element.android.libraries.matrix.test.core.aBuildMeta
import org.junit.Test
class TracingMatrixModuleTest {
@Test
fun `providesTracingFilterConfiguration returns debug config for debug build`() {
assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.DEBUG)))
.isEqualTo(TracingFilterConfigurations.debug)
}
@Test
fun `providesTracingFilterConfiguration returns nightly config for nightly build`() {
assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.NIGHTLY)))
.isEqualTo(TracingFilterConfigurations.nightly)
}
@Test
fun `providesTracingFilterConfiguration returns release config for release build`() {
assertThat(TracingMatrixModule.providesTracingFilterConfiguration(aBuildMeta(BuildType.RELEASE)))
.isEqualTo(TracingFilterConfigurations.release)
}
}

View file

@ -37,6 +37,7 @@ fun aRustEventTimelineItem(
origin: EventItemOrigin? = EventItemOrigin.SYNC, origin: EventItemOrigin? = EventItemOrigin.SYNC,
canBeRepliedTo: Boolean = true, canBeRepliedTo: Boolean = true,
shieldsState: ShieldState? = null, shieldsState: ShieldState? = null,
localCreatedAt: ULong? = null,
) = EventTimelineItem( ) = EventTimelineItem(
isRemote = isRemote, isRemote = isRemote,
eventOrTransactionId = eventOrTransactionId, eventOrTransactionId = eventOrTransactionId,
@ -51,6 +52,7 @@ fun aRustEventTimelineItem(
reactions = reactions, reactions = reactions,
readReceipts = readReceipts, readReceipts = readReceipts,
origin = origin, origin = origin,
localCreatedAt = localCreatedAt,
lazyProvider = FakeRustLazyTimelineItemProvider( lazyProvider = FakeRustLazyTimelineItemProvider(
debugInfo = debugInfo, debugInfo = debugInfo,
shieldsState = shieldsState, shieldsState = shieldsState,

View file

@ -13,6 +13,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
import org.matrix.rustcomponents.sdk.JoinRule import org.matrix.rustcomponents.sdk.JoinRule
import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.Membership
import org.matrix.rustcomponents.sdk.RoomHero import org.matrix.rustcomponents.sdk.RoomHero
import org.matrix.rustcomponents.sdk.RoomHistoryVisibility
import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfo
import org.matrix.rustcomponents.sdk.RoomMember import org.matrix.rustcomponents.sdk.RoomMember
import org.matrix.rustcomponents.sdk.RoomNotificationMode import org.matrix.rustcomponents.sdk.RoomNotificationMode
@ -49,6 +50,7 @@ fun aRustRoomInfo(
pinnedEventIds: List<String> = listOf(), pinnedEventIds: List<String> = listOf(),
roomCreator: UserId? = null, roomCreator: UserId? = null,
joinRule: JoinRule? = null, joinRule: JoinRule? = null,
historyVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Joined,
) = RoomInfo( ) = RoomInfo(
id = id, id = id,
displayName = displayName, displayName = displayName,
@ -81,4 +83,5 @@ fun aRustRoomInfo(
pinnedEventIds = pinnedEventIds, pinnedEventIds = pinnedEventIds,
creator = roomCreator?.value, creator = roomCreator?.value,
joinRule = joinRule, joinRule = joinRule,
historyVisibility = historyVisibility
) )

View file

@ -21,6 +21,7 @@ fun aRustRoomMember(
powerLevel: Long = 0L, powerLevel: Long = 0L,
isIgnored: Boolean = false, isIgnored: Boolean = false,
role: RoomMemberRole = RoomMemberRole.USER, role: RoomMemberRole = RoomMemberRole.USER,
membershipChangeReason: String? = null,
) = RoomMember( ) = RoomMember(
userId = userId.value, userId = userId.value,
displayName = displayName, displayName = displayName,
@ -31,4 +32,5 @@ fun aRustRoomMember(
normalizedPowerLevel = powerLevel, normalizedPowerLevel = powerLevel,
isIgnored = isIgnored, isIgnored = isIgnored,
suggestedRoleForPowerLevel = role, suggestedRoleForPowerLevel = role,
membershipChangeReason = membershipChangeReason,
) )

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.preferences.api.store package io.element.android.libraries.preferences.api.store
import io.element.android.libraries.matrix.api.tracing.LogLevel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AppPreferencesStore { interface AppPreferencesStore {
@ -25,5 +26,8 @@ interface AppPreferencesStore {
suspend fun setHideImagesAndVideos(value: Boolean) suspend fun setHideImagesAndVideos(value: Boolean)
fun doesHideImagesAndVideosFlow(): Flow<Boolean> fun doesHideImagesAndVideosFlow(): Flow<Boolean>
suspend fun setTracingLogLevel(logLevel: LogLevel)
fun getTracingLogLevelFlow(): Flow<LogLevel>
suspend fun reset() suspend fun reset()
} }

View file

@ -19,6 +19,7 @@ import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -31,6 +32,7 @@ private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseU
private val themeKey = stringPreferencesKey("theme") private val themeKey = stringPreferencesKey("theme")
private val simplifiedSlidingSyncKey = booleanPreferencesKey("useSimplifiedSlidingSync") private val simplifiedSlidingSyncKey = booleanPreferencesKey("useSimplifiedSlidingSync")
private val hideImagesAndVideosKey = booleanPreferencesKey("hideImagesAndVideos") private val hideImagesAndVideosKey = booleanPreferencesKey("hideImagesAndVideos")
private val logLevelKey = stringPreferencesKey("logLevel")
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultAppPreferencesStore @Inject constructor( class DefaultAppPreferencesStore @Inject constructor(
@ -104,7 +106,27 @@ class DefaultAppPreferencesStore @Inject constructor(
} }
} }
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 reset() { override suspend fun reset() {
store.edit { it.clear() } 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

@ -7,6 +7,7 @@
package io.element.android.libraries.preferences.test package io.element.android.libraries.preferences.test
import io.element.android.libraries.matrix.api.tracing.LogLevel
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -16,13 +17,15 @@ class InMemoryAppPreferencesStore(
hideImagesAndVideos: Boolean = false, hideImagesAndVideos: Boolean = false,
customElementCallBaseUrl: String? = null, customElementCallBaseUrl: String? = null,
theme: String? = null, theme: String? = null,
simplifiedSlidingSyncEnabled: Boolean = false simplifiedSlidingSyncEnabled: Boolean = false,
logLevel: LogLevel = LogLevel.INFO,
) : AppPreferencesStore { ) : AppPreferencesStore {
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled) private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
private val hideImagesAndVideos = MutableStateFlow(hideImagesAndVideos) private val hideImagesAndVideos = MutableStateFlow(hideImagesAndVideos)
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl) private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
private val theme = MutableStateFlow(theme) private val theme = MutableStateFlow(theme)
private val simplifiedSlidingSyncEnabled = MutableStateFlow(simplifiedSlidingSyncEnabled) private val simplifiedSlidingSyncEnabled = MutableStateFlow(simplifiedSlidingSyncEnabled)
private val logLevel = MutableStateFlow(logLevel)
override suspend fun setDeveloperModeEnabled(enabled: Boolean) { override suspend fun setDeveloperModeEnabled(enabled: Boolean) {
isDeveloperModeEnabled.value = enabled isDeveloperModeEnabled.value = enabled
@ -64,6 +67,14 @@ class InMemoryAppPreferencesStore(
return hideImagesAndVideos return hideImagesAndVideos
} }
override suspend fun setTracingLogLevel(logLevel: LogLevel) {
this.logLevel.value = logLevel
}
override fun getTracingLogLevelFlow(): Flow<LogLevel> {
return logLevel
}
override suspend fun reset() { override suspend fun reset() {
// No op // No op
} }

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4f756552f2d88693ae654f92c24b5c23a31cfa6086309e0996444f17bd33f07a
size 312449

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6992b4234fab8b623c05c01c6823791f09bf561b83052cd57d69952380762557
size 393746

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f1df59fddb1576e385b2fb02cefa904b445376ceb4f1d6ee307e1fcd897d25d
size 38164

View file

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:98bd0b6bf5d549ccb7c306dedc807aeb6bd98e77be7b6d8f73ce444000872624
size 36727

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca9a1d1bb7d4de0b8a5f5efd62cde248f0c9e07b88b2fbdbc598d922003a7f02
size 31032