From 64ff19c808be9abd3606b840dbf12ddc82068257 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Oct 2025 14:42:53 +0200 Subject: [PATCH 1/5] Update API around brandColor. --- enterprise | 2 +- .../features/enterprise/api/EnterpriseService.kt | 2 +- .../enterprise/impl/DefaultEnterpriseService.kt | 2 +- .../enterprise/test/FakeEnterpriseService.kt | 2 +- .../impl/developer/DeveloperSettingsPresenter.kt | 2 +- .../impl/developer/DeveloperSettingsPresenterTest.kt | 1 + .../libraries/sessionstorage/api/SessionStore.kt | 12 ++++++++++++ .../libraries/wellknown/api/ElementWellKnown.kt | 1 + .../wellknown/impl/InternalElementWellKnown.kt | 2 ++ .../android/libraries/wellknown/impl/Mapper.kt | 1 + .../impl/DefaultSessionWellknownRetrieverTest.kt | 6 +++++- .../android/features/wellknown/test/Fixtures.kt | 2 ++ 12 files changed, 29 insertions(+), 6 deletions(-) diff --git a/enterprise b/enterprise index c5465c9579..51fe0a48eb 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit c5465c95792004409e0eaa7342171e1cd652914a +Subproject commit 51fe0a48eb11c7d67da6d598820b06d7d30bf8e9 diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 5e5e45ffb9..8eb4184fec 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -23,7 +23,7 @@ interface EnterpriseService { * Override the brand color. * @param brandColor the color in hex format (#RRGGBBAA or #RRGGBB), or null to reset to default. */ - fun overrideBrandColor(brandColor: String?) + suspend fun overrideBrandColor(brandColor: String?) @Composable fun semanticColorsLight(): State diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index 6251a0b4e6..924a9aec26 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -32,7 +32,7 @@ class DefaultEnterpriseService : EnterpriseService { override fun defaultHomeserverList(): List = emptyList() override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true - override fun overrideBrandColor(brandColor: String?) = Unit + override suspend fun overrideBrandColor(brandColor: String?) = Unit @Composable override fun semanticColorsLight(): State { diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index f2e597c6fa..04aa9dd640 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -42,7 +42,7 @@ class FakeEnterpriseService( isAllowedToConnectToHomeserverResult(homeserverUrl) } - override fun overrideBrandColor(brandColor: String?) { + override suspend fun overrideBrandColor(brandColor: String?) = simulateLongTask { overrideBrandColorResult(brandColor) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 85aadbb06b..9ed2abe609 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -135,7 +135,7 @@ class DeveloperSettingsPresenter( } appPreferencesStore.setTracingLogPacks(currentPacks) } - is DeveloperSettingsEvents.ChangeBrandColor -> { + is DeveloperSettingsEvents.ChangeBrandColor -> coroutineScope.launch { showColorPicker = false val color = event.color?.value?.toHexString(HexFormat.UpperCase)?.substring(2, 8) enterpriseService.overrideBrandColor(color) diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index f13d5b3cd8..593b54f077 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -203,6 +203,7 @@ class DeveloperSettingsPresenterTest { assertThat(awaitItem().showColorPicker).isTrue() initialState.eventSink(DeveloperSettingsEvents.ChangeBrandColor(Color.Green)) assertThat(awaitItem().showColorPicker).isFalse() + skipItems(1) overrideBrandColorResult.assertions().isCalledOnce() .with(value("00FF00")) } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index 9d9f143e15..96d6c4a68e 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -73,3 +73,15 @@ fun List.toUserList(): List { fun Flow>.toUserListFlow(): Flow> { return map { it.toUserList() } } + +/** + * @return a flow emitting the userId of the latest session if logged in, null otherwise. + */ +fun SessionStore.userIdFlow(): Flow { + return loggedInStateFlow().map { + when (it) { + is LoggedInState.LoggedIn -> it.sessionId + is LoggedInState.NotLoggedIn -> null + } + } +} diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt index 064416eec1..6f1384422c 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt @@ -11,4 +11,5 @@ data class ElementWellKnown( val registrationHelperUrl: String?, val enforceElementPro: Boolean?, val rageshakeUrl: String?, + val brandColor: String?, ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt index e81d78d498..2a0ba1ee72 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt @@ -27,4 +27,6 @@ data class InternalElementWellKnown( val enforceElementPro: Boolean? = null, @SerialName("rageshake_url") val rageshakeUrl: String? = null, + @SerialName("brand_color") + val brandColor: String? = null, ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt index 9c1618f699..169757caa9 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt @@ -15,6 +15,7 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, rageshakeUrl = rageshakeUrl, + brandColor = brandColor, ) internal fun InternalWellKnown.map() = WellKnown( diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt index d19530befb..9356648a3a 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultSessionWellknownRetrieverTest.kt @@ -161,6 +161,7 @@ class DefaultSessionWellknownRetrieverTest { registrationHelperUrl = null, enforceElementPro = null, rageshakeUrl = null, + brandColor = null, ) ) getUrlLambda.assertions().isCalledOnce() @@ -175,7 +176,8 @@ class DefaultSessionWellknownRetrieverTest { """{ "registration_helper_url": "a_registration_url", "enforce_element_pro": true, - "rageshake_url": "a_rageshake_url" + "rageshake_url": "a_rageshake_url", + "brand_color": "#FF0000" }""".trimIndent().toByteArray() ) } @@ -185,6 +187,7 @@ class DefaultSessionWellknownRetrieverTest { registrationHelperUrl = "a_registration_url", enforceElementPro = true, rageshakeUrl = "a_rageshake_url", + brandColor = "#FF0000", ) ) } @@ -208,6 +211,7 @@ class DefaultSessionWellknownRetrieverTest { registrationHelperUrl = "a_registration_url", enforceElementPro = true, rageshakeUrl = "a_rageshake_url", + brandColor = null, ) ) } diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt index 686026b78d..26eedc7cde 100644 --- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt @@ -13,8 +13,10 @@ fun anElementWellKnown( registrationHelperUrl: String? = null, enforceElementPro: Boolean? = null, rageshakeUrl: String? = null, + brandColor: String? = null, ) = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, rageshakeUrl = rageshakeUrl, + brandColor = brandColor, ) From c4884879d8f93eacf9cea310b7b596efe3825d44 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Oct 2025 15:51:55 +0200 Subject: [PATCH 2/5] Avoid emitted a new value each time the token is refreshed (for instance) --- .../libraries/sessionstorage/impl/DatabaseSessionStore.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt index d6197d868d..80995e27c6 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStore.kt @@ -19,6 +19,7 @@ import io.element.android.libraries.sessionstorage.api.LoggedInState import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -47,6 +48,7 @@ class DatabaseSessionStore( ) } } + .distinctUntilChanged() } override suspend fun addSession(sessionData: SessionData) { From 0e4a3c8d12b6ea00e3c782fb1256baba6b29c99a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Oct 2025 15:57:08 +0200 Subject: [PATCH 3/5] Bug reporter: ensure the log are store in the correct folder. --- .../io/element/android/appnav/RootFlowNode.kt | 3 - .../rageshake/api/reporter/BugReporter.kt | 8 - .../impl/reporter/DefaultBugReporter.kt | 35 +++-- .../impl/bugreport/FakeBugReporter.kt | 4 - .../impl/reporter/DefaultBugReporterTest.kt | 147 ++++++++---------- 5 files changed, 84 insertions(+), 113 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 19290c5f8b..6d635df840 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -38,7 +38,6 @@ import io.element.android.features.announcement.api.AnnouncementService import io.element.android.features.login.api.LoginParams import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint -import io.element.android.features.rageshake.api.reporter.BugReporter import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint import io.element.android.libraries.architecture.BackstackView @@ -80,7 +79,6 @@ class RootFlowNode( private val accountSelectEntryPoint: AccountSelectEntryPoint, private val intentResolver: IntentResolver, private val oidcActionFlow: OidcActionFlow, - private val bugReporter: BugReporter, private val featureFlagService: FeatureFlagService, private val announcementService: AnnouncementService, ) : BaseFlowNode( @@ -130,7 +128,6 @@ class RootFlowNode( private fun switchToNotLoggedInFlow(params: LoginParams?) { matrixSessionCache.removeAll() - bugReporter.setLogDirectorySubfolder(null) backstack.safeRoot(NavTarget.NotLoggedInFlow(params)) } diff --git a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt index 02e53fbb1c..6d14ca63bf 100644 --- a/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt +++ b/features/rageshake/api/src/main/kotlin/io/element/android/features/rageshake/api/reporter/BugReporter.kt @@ -36,14 +36,6 @@ interface BugReporter { */ fun logDirectory(): File - /** - * Set the subfolder name for the log directory. - * This will create a subfolder in the log directory with the given name. - * It will also configure the Rust SDK to use this subfolder for its logs. - * If the name is null, the log files will be stored in the base folder for the logs. - */ - fun setLogDirectorySubfolder(subfolderName: String?) - /** * Set the current tracing log level. */ diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 5eecd17f31..2584ebe597 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -28,16 +28,22 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.SdkMetadata -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.userIdFlow import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -67,6 +73,8 @@ import java.util.Locale @Inject class DefaultBugReporter( @ApplicationContext private val context: Context, + @AppCoroutineScope + private val appCoroutineScope: CoroutineScope, private val screenshotHolder: ScreenshotHolder, private val crashDataStore: CrashDataStore, private val coroutineDispatchers: CoroutineDispatchers, @@ -78,7 +86,6 @@ class DefaultBugReporter( private val sdkMetadata: SdkMetadata, private val matrixClientProvider: MatrixClientProvider, private val tracingService: TracingService, - matrixAuthenticationService: MatrixAuthenticationService, ) : BugReporter { companion object { // filenames @@ -98,13 +105,18 @@ class DefaultBugReporter( if (buildMeta.isEnterpriseBuild) { val logSubfolder = runBlocking { sessionStore.getLatestSession() - }?.userId?.substringAfter(":") + }?.userId?.let(::UserId)?.domainName setCurrentLogDirectory(logSubfolder) - matrixAuthenticationService.listenToNewMatrixClients { - // When a new Matrix client is created, we update the tracing configuration to write - // the files in a dedicated subfolders. - setLogDirectorySubfolder(it.userIdServerName()) - } + sessionStore.userIdFlow() + .map { + it?.let(::UserId)?.domainName + } + .distinctUntilChanged() + .onEach { logSubfolder -> + setCurrentLogDirectory(logSubfolder) + tracingService.updateWriteToFilesConfiguration(createWriteToFilesConfiguration()) + } + .launchIn(appCoroutineScope) } } @@ -335,13 +347,6 @@ class DefaultBugReporter( } } - override fun setLogDirectorySubfolder(subfolderName: String?) { - if (buildMeta.isEnterpriseBuild) { - setCurrentLogDirectory(subfolderName) - tracingService.updateWriteToFilesConfiguration(createWriteToFilesConfiguration()) - } - } - private fun setCurrentLogDirectory(subfolderName: String?) { currentLogDirectory = if (subfolderName == null) { baseLogDirectory diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt index bd9c538c0d..15a6c7d456 100644 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/bugreport/FakeBugReporter.kt @@ -54,10 +54,6 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter { return File("fake") } - override fun setLogDirectorySubfolder(subfolderName: String?) { - // No op - } - override fun setCurrentTracingLogLevel(logLevel: String) { // No op } diff --git a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt index 794c5b56e0..ddc25b37f4 100755 --- a/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt +++ b/features/rageshake/impl/src/test/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporterTest.kt @@ -15,7 +15,6 @@ import io.element.android.features.rageshake.impl.crash.FakeCrashDataStore import io.element.android.features.rageshake.impl.screenshot.FakeScreenshotHolder import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.MatrixClientProvider -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import io.element.android.libraries.matrix.test.A_DEVICE_ID @@ -23,7 +22,6 @@ import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeSdkMetadata -import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService @@ -34,8 +32,10 @@ import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import okhttp3.MultipartReader import okhttp3.OkHttpClient @@ -405,53 +405,85 @@ class DefaultBugReporterTest { assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") } + @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `when the log directory is updated, the tracing service is invoked`() = runTest { + fun `when a session is added, the tracing service is invoked`() = runTest { var param: WriteToFilesConfiguration? = null val updateWriteToFilesConfigurationResult = lambdaRecorder { param = it } - val sut = createDefaultBugReporter( + val sessionStore = InMemorySessionStore() + createDefaultBugReporter( buildMeta = aBuildMeta(isEnterpriseBuild = true), + sessionStore = sessionStore, tracingService = FakeTracingService( updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, ), ) - sut.setLogDirectorySubfolder("my.sub.folder") + sessionStore.addSession(aSessionData(sessionId = "@alice:server.org")) + runCurrent() updateWriteToFilesConfigurationResult.assertions().isCalledOnce() assertThat(param).isNotNull() assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java) - assertThat((param as WriteToFilesConfiguration.Enabled).directory).endsWith("/cache/logs/my.sub.folder") + assertThat((param as WriteToFilesConfiguration.Enabled).directory).endsWith("/cache/logs/server.org") assertThat((param as WriteToFilesConfiguration.Enabled).filenamePrefix).isEqualTo("logs") assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168) assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log") } + @OptIn(ExperimentalCoroutinesApi::class) @Test - fun `foss build - when the log directory is updated, the tracing service is not invoked`() = runTest { + fun `when another session is added on same domain, the tracing service is not invoked`() = runTest { val updateWriteToFilesConfigurationResult = lambdaRecorder {} - val sut = createDefaultBugReporter( - tracingService = FakeTracingService( - updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, - ) - ) - sut.setLogDirectorySubfolder("my.sub.folder") - updateWriteToFilesConfigurationResult.assertions().isNeverCalled() - } - - @Test - fun `when the log directory is reset, the tracing service is invoked`() = runTest { - var param: WriteToFilesConfiguration? = null - val updateWriteToFilesConfigurationResult = lambdaRecorder { - param = it - } - val sut = createDefaultBugReporter( + val sessionStore = InMemorySessionStore() + createDefaultBugReporter( buildMeta = aBuildMeta(isEnterpriseBuild = true), + sessionStore = sessionStore, tracingService = FakeTracingService( updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, ), ) - sut.setLogDirectorySubfolder(null) + sessionStore.addSession(aSessionData(sessionId = "@alice:server.org")) + runCurrent() + updateWriteToFilesConfigurationResult.assertions().isCalledOnce() + sessionStore.addSession(aSessionData(sessionId = "@bob:server.org")) + runCurrent() + updateWriteToFilesConfigurationResult.assertions().isCalledOnce() + } + + @Test + fun `foss build - when a session is added, the tracing service is not invoked`() = runTest { + val updateWriteToFilesConfigurationResult = lambdaRecorder {} + val sessionStore = InMemorySessionStore() + createDefaultBugReporter( + tracingService = FakeTracingService( + updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, + ), + sessionStore = sessionStore, + ) + sessionStore.addSession(aSessionData(sessionId = "@alice:server.org")) + updateWriteToFilesConfigurationResult.assertions().isNeverCalled() + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun `when the user signs out, the tracing service is invoked`() = runTest { + var param: WriteToFilesConfiguration? = null + val updateWriteToFilesConfigurationResult = lambdaRecorder { + param = it + } + val sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData(sessionId = "@alice:server.org")), + ) + createDefaultBugReporter( + buildMeta = aBuildMeta(isEnterpriseBuild = true), + tracingService = FakeTracingService( + updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, + ), + sessionStore = sessionStore, + ) + sessionStore.removeSession("@alice:server.org") + runCurrent() updateWriteToFilesConfigurationResult.assertions().isCalledOnce() assertThat(param).isNotNull() assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java) @@ -464,66 +496,16 @@ class DefaultBugReporterTest { @Test fun `foss build - when the log directory is reset, the tracing service is not invoked`() = runTest { val updateWriteToFilesConfigurationResult = lambdaRecorder {} - val sut = createDefaultBugReporter( + val sessionStore = InMemorySessionStore( + initialList = listOf(aSessionData(sessionId = "@alice:server.org")), + ) + createDefaultBugReporter( tracingService = FakeTracingService( updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, - ) + ), + sessionStore = sessionStore, ) - sut.setLogDirectorySubfolder(null) - updateWriteToFilesConfigurationResult.assertions().isNeverCalled() - } - - @Test - fun `when a new MatrixClient is created the logs folder is updated`() = runTest { - var param: WriteToFilesConfiguration? = null - val updateWriteToFilesConfigurationResult = lambdaRecorder { - param = it - } - val matrixAuthenticationService = FakeMatrixAuthenticationService().apply { - givenMatrixClient( - FakeMatrixClient( - userIdServerNameLambda = { "domain.foo.org" }, - ) - ) - } - val sut = createDefaultBugReporter( - buildMeta = aBuildMeta(isEnterpriseBuild = true), - matrixAuthenticationService = matrixAuthenticationService, - tracingService = FakeTracingService( - updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, - ) - ) - assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") - matrixAuthenticationService.login("alice", "password") - assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs/domain.foo.org") - updateWriteToFilesConfigurationResult.assertions().isCalledOnce() - assertThat(param).isNotNull() - assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java) - assertThat((param as WriteToFilesConfiguration.Enabled).directory).endsWith("/cache/logs/domain.foo.org") - assertThat((param as WriteToFilesConfiguration.Enabled).filenamePrefix).isEqualTo("logs") - assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168) - assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log") - } - - @Test - fun `foss build - when a new MatrixClient is created the logs folder is not updated`() = runTest { - val updateWriteToFilesConfigurationResult = lambdaRecorder {} - val matrixAuthenticationService = FakeMatrixAuthenticationService().apply { - givenMatrixClient( - FakeMatrixClient( - userIdServerNameLambda = { "domain.foo.org" }, - ) - ) - } - val sut = createDefaultBugReporter( - matrixAuthenticationService = matrixAuthenticationService, - tracingService = FakeTracingService( - updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, - ) - ) - assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") - matrixAuthenticationService.login("alice", "password") - assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") + sessionStore.removeSession("@alice:server.org") updateWriteToFilesConfigurationResult.assertions().isNeverCalled() } @@ -534,10 +516,10 @@ class DefaultBugReporterTest { crashDataStore: CrashDataStore = FakeCrashDataStore(), server: MockWebServer = MockWebServer(), tracingService: TracingService = FakeTracingService(), - matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), ): DefaultBugReporter { return DefaultBugReporter( context = RuntimeEnvironment.getApplication(), + appCoroutineScope = backgroundScope, screenshotHolder = FakeScreenshotHolder(), crashDataStore = crashDataStore, coroutineDispatchers = testCoroutineDispatchers(), @@ -549,7 +531,6 @@ class DefaultBugReporterTest { sdkMetadata = FakeSdkMetadata("123456789"), matrixClientProvider = matrixClientProvider, tracingService = tracingService, - matrixAuthenticationService = matrixAuthenticationService, ) } From 0f3858649ca69219e2731df50c7a42e08b45ee6a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 20 Oct 2025 16:56:36 +0200 Subject: [PATCH 4/5] Update ref --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 51fe0a48eb..f9178b5a11 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 51fe0a48eb11c7d67da6d598820b06d7d30bf8e9 +Subproject commit f9178b5a11cda8d91ae5d62d6ad66ae8a6b3081e From 64b5b535104f818d3ce63b51e4a60026e5252d4d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 21 Oct 2025 11:53:36 +0200 Subject: [PATCH 5/5] Improve API and documentation --- enterprise | 2 +- .../android/features/enterprise/api/EnterpriseService.kt | 3 ++- .../features/enterprise/impl/DefaultEnterpriseService.kt | 2 +- .../enterprise/impl/DefaultEnterpriseServiceTest.kt | 4 ++-- .../features/enterprise/test/FakeEnterpriseService.kt | 6 +++--- .../impl/developer/DeveloperSettingsPresenter.kt | 4 +++- .../impl/developer/DeveloperSettingsPresenterTest.kt | 8 ++++++-- .../rageshake/impl/reporter/DefaultBugReporter.kt | 4 ++-- .../android/libraries/sessionstorage/api/SessionStore.kt | 4 ++-- 9 files changed, 22 insertions(+), 15 deletions(-) diff --git a/enterprise b/enterprise index f9178b5a11..f662f079f9 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit f9178b5a11cda8d91ae5d62d6ad66ae8a6b3081e +Subproject commit f662f079f911b728e5769d10268e2c2775d7287a diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 8eb4184fec..2f62327643 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -21,9 +21,10 @@ interface EnterpriseService { /** * Override the brand color. + * @param sessionId the session to override the brand color for, or null to set the brand color to use when there is no session. * @param brandColor the color in hex format (#RRGGBBAA or #RRGGBB), or null to reset to default. */ - suspend fun overrideBrandColor(brandColor: String?) + suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) @Composable fun semanticColorsLight(): State diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index 924a9aec26..f2171c4a49 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -32,7 +32,7 @@ class DefaultEnterpriseService : EnterpriseService { override fun defaultHomeserverList(): List = emptyList() override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true - override suspend fun overrideBrandColor(brandColor: String?) = Unit + override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit @Composable override fun semanticColorsLight(): State { diff --git a/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt index d3a4a63ad1..5701aa6574 100644 --- a/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt +++ b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt @@ -51,7 +51,7 @@ class DefaultEnterpriseServiceTest { }.test { val initialState = awaitItem() assertThat(initialState).isEqualTo(compoundColorsLight) - defaultEnterpriseService.overrideBrandColor("#87654321") + defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "#87654321") expectNoEvents() } } @@ -64,7 +64,7 @@ class DefaultEnterpriseServiceTest { }.test { val initialState = awaitItem() assertThat(initialState).isEqualTo(compoundColorsDark) - defaultEnterpriseService.overrideBrandColor("#87654321") + defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "#87654321") expectNoEvents() } } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index 04aa9dd640..64f2898078 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -26,7 +26,7 @@ class FakeEnterpriseService( private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, private val semanticColorsLightResult: () -> State = { lambdaError() }, private val semanticColorsDarkResult: () -> State = { lambdaError() }, - private val overrideBrandColorResult: (String?) -> Unit = { lambdaError() }, + private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, ) : EnterpriseService { @@ -42,8 +42,8 @@ class FakeEnterpriseService( isAllowedToConnectToHomeserverResult(homeserverUrl) } - override suspend fun overrideBrandColor(brandColor: String?) = simulateLongTask { - overrideBrandColorResult(brandColor) + override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask { + overrideBrandColorResult(sessionId, brandColor) } @Composable diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index 9ed2abe609..1e68652e5c 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.ui.model.FeatureUiModel +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.preferences.api.store.AppPreferencesStore import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -50,6 +51,7 @@ import java.net.URL @Inject class DeveloperSettingsPresenter( + private val sessionId: SessionId, private val featureFlagService: FeatureFlagService, private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, private val clearCacheUseCase: ClearCacheUseCase, @@ -138,7 +140,7 @@ class DeveloperSettingsPresenter( is DeveloperSettingsEvents.ChangeBrandColor -> coroutineScope.launch { showColorPicker = false val color = event.color?.value?.toHexString(HexFormat.UpperCase)?.substring(2, 8) - enterpriseService.overrideBrandColor(color) + enterpriseService.overrideBrandColor(sessionId, color) } is DeveloperSettingsEvents.SetShowColorPicker -> { showColorPicker = event.show diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 593b54f077..b68c01266d 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -25,6 +25,8 @@ import io.element.android.libraries.featureflag.api.Feature import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeature import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule @@ -184,7 +186,7 @@ class DeveloperSettingsPresenterTest { @Test fun `present - enterprise build can change the brand color`() = runTest { - val overrideBrandColorResult = lambdaRecorder { } + val overrideBrandColorResult = lambdaRecorder { _, _ -> } val presenter = createDeveloperSettingsPresenter( enterpriseService = FakeEnterpriseService( isEnterpriseBuild = true, @@ -205,11 +207,12 @@ class DeveloperSettingsPresenterTest { assertThat(awaitItem().showColorPicker).isFalse() skipItems(1) overrideBrandColorResult.assertions().isCalledOnce() - .with(value("00FF00")) + .with(value(A_SESSION_ID), value("00FF00")) } } private fun createDeveloperSettingsPresenter( + sessionId: SessionId = A_SESSION_ID, featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( getAvailableFeaturesResult = { _, _ -> listOf( @@ -228,6 +231,7 @@ class DeveloperSettingsPresenterTest { enterpriseService: EnterpriseService = FakeEnterpriseService(), ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( + sessionId = sessionId, featureFlagService = featureFlagService, computeCacheSizeUseCase = cacheSizeUseCase, clearCacheUseCase = clearCacheUseCase, diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt index 2584ebe597..c84388b099 100755 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/reporter/DefaultBugReporter.kt @@ -36,7 +36,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.api.userIdFlow +import io.element.android.libraries.sessionstorage.api.sessionIdFlow import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged @@ -107,7 +107,7 @@ class DefaultBugReporter( sessionStore.getLatestSession() }?.userId?.let(::UserId)?.domainName setCurrentLogDirectory(logSubfolder) - sessionStore.userIdFlow() + sessionStore.sessionIdFlow() .map { it?.let(::UserId)?.domainName } diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt index 96d6c4a68e..7900b4d90d 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionStore.kt @@ -75,9 +75,9 @@ fun Flow>.toUserListFlow(): Flow> { } /** - * @return a flow emitting the userId of the latest session if logged in, null otherwise. + * @return a flow emitting the sessionId of the latest session if logged in, null otherwise. */ -fun SessionStore.userIdFlow(): Flow { +fun SessionStore.sessionIdFlow(): Flow { return loggedInStateFlow().map { when (it) { is LoggedInState.LoggedIn -> it.sessionId