Merge pull request #5565 from element-hq/bma/wellknownBrandColor

Improve code around Element .well-known configuration
This commit is contained in:
Benoit Marty 2025-10-21 14:15:30 +02:00 committed by GitHub
commit f8628840ca
19 changed files with 129 additions and 126 deletions

View file

@ -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.LoginParams
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl 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.bugreport.BugReportEntryPoint
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.signedout.api.SignedOutEntryPoint import io.element.android.features.signedout.api.SignedOutEntryPoint
import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint import io.element.android.libraries.accountselect.api.AccountSelectEntryPoint
import io.element.android.libraries.architecture.BackstackView import io.element.android.libraries.architecture.BackstackView
@ -80,7 +79,6 @@ class RootFlowNode(
private val accountSelectEntryPoint: AccountSelectEntryPoint, private val accountSelectEntryPoint: AccountSelectEntryPoint,
private val intentResolver: IntentResolver, private val intentResolver: IntentResolver,
private val oidcActionFlow: OidcActionFlow, private val oidcActionFlow: OidcActionFlow,
private val bugReporter: BugReporter,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val announcementService: AnnouncementService, private val announcementService: AnnouncementService,
) : BaseFlowNode<RootFlowNode.NavTarget>( ) : BaseFlowNode<RootFlowNode.NavTarget>(
@ -130,7 +128,6 @@ class RootFlowNode(
private fun switchToNotLoggedInFlow(params: LoginParams?) { private fun switchToNotLoggedInFlow(params: LoginParams?) {
matrixSessionCache.removeAll() matrixSessionCache.removeAll()
bugReporter.setLogDirectorySubfolder(null)
backstack.safeRoot(NavTarget.NotLoggedInFlow(params)) backstack.safeRoot(NavTarget.NotLoggedInFlow(params))
} }

@ -1 +1 @@
Subproject commit c5465c95792004409e0eaa7342171e1cd652914a Subproject commit f662f079f911b728e5769d10268e2c2775d7287a

View file

@ -21,9 +21,10 @@ interface EnterpriseService {
/** /**
* Override the brand color. * 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. * @param brandColor the color in hex format (#RRGGBBAA or #RRGGBB), or null to reset to default.
*/ */
fun overrideBrandColor(brandColor: String?) suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
@Composable @Composable
fun semanticColorsLight(): State<SemanticColors> fun semanticColorsLight(): State<SemanticColors>

View file

@ -32,7 +32,7 @@ class DefaultEnterpriseService : EnterpriseService {
override fun defaultHomeserverList(): List<String> = emptyList() override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override fun overrideBrandColor(brandColor: String?) = Unit override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
@Composable @Composable
override fun semanticColorsLight(): State<SemanticColors> { override fun semanticColorsLight(): State<SemanticColors> {

View file

@ -51,7 +51,7 @@ class DefaultEnterpriseServiceTest {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState).isEqualTo(compoundColorsLight) assertThat(initialState).isEqualTo(compoundColorsLight)
defaultEnterpriseService.overrideBrandColor("#87654321") defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "#87654321")
expectNoEvents() expectNoEvents()
} }
} }
@ -64,7 +64,7 @@ class DefaultEnterpriseServiceTest {
}.test { }.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState).isEqualTo(compoundColorsDark) assertThat(initialState).isEqualTo(compoundColorsDark)
defaultEnterpriseService.overrideBrandColor("#87654321") defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "#87654321")
expectNoEvents() expectNoEvents()
} }
} }

View file

@ -26,7 +26,7 @@ class FakeEnterpriseService(
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> State<SemanticColors> = { lambdaError() }, private val semanticColorsLightResult: () -> State<SemanticColors> = { lambdaError() },
private val semanticColorsDarkResult: () -> State<SemanticColors> = { lambdaError() }, private val semanticColorsDarkResult: () -> State<SemanticColors> = { lambdaError() },
private val overrideBrandColorResult: (String?) -> Unit = { lambdaError() }, private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
) : EnterpriseService { ) : EnterpriseService {
@ -42,8 +42,8 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl) isAllowedToConnectToHomeserverResult(homeserverUrl)
} }
override fun overrideBrandColor(brandColor: String?) { override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
overrideBrandColorResult(brandColor) overrideBrandColorResult(sessionId, brandColor)
} }
@Composable @Composable

View file

@ -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.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.ui.model.FeatureUiModel 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 io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -50,6 +51,7 @@ import java.net.URL
@Inject @Inject
class DeveloperSettingsPresenter( class DeveloperSettingsPresenter(
private val sessionId: SessionId,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val computeCacheSizeUseCase: ComputeCacheSizeUseCase, private val computeCacheSizeUseCase: ComputeCacheSizeUseCase,
private val clearCacheUseCase: ClearCacheUseCase, private val clearCacheUseCase: ClearCacheUseCase,
@ -135,10 +137,10 @@ class DeveloperSettingsPresenter(
} }
appPreferencesStore.setTracingLogPacks(currentPacks) appPreferencesStore.setTracingLogPacks(currentPacks)
} }
is DeveloperSettingsEvents.ChangeBrandColor -> { is DeveloperSettingsEvents.ChangeBrandColor -> coroutineScope.launch {
showColorPicker = false showColorPicker = false
val color = event.color?.value?.toHexString(HexFormat.UpperCase)?.substring(2, 8) val color = event.color?.value?.toHexString(HexFormat.UpperCase)?.substring(2, 8)
enterpriseService.overrideBrandColor(color) enterpriseService.overrideBrandColor(sessionId, color)
} }
is DeveloperSettingsEvents.SetShowColorPicker -> { is DeveloperSettingsEvents.SetShowColorPicker -> {
showColorPicker = event.show showColorPicker = event.show

View file

@ -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.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeature import io.element.android.libraries.featureflag.test.FakeFeature
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService 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.matrix.test.core.aBuildMeta
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.WarmUpRule
@ -184,7 +186,7 @@ class DeveloperSettingsPresenterTest {
@Test @Test
fun `present - enterprise build can change the brand color`() = runTest { fun `present - enterprise build can change the brand color`() = runTest {
val overrideBrandColorResult = lambdaRecorder<String?, Unit> { } val overrideBrandColorResult = lambdaRecorder<SessionId?, String?, Unit> { _, _ -> }
val presenter = createDeveloperSettingsPresenter( val presenter = createDeveloperSettingsPresenter(
enterpriseService = FakeEnterpriseService( enterpriseService = FakeEnterpriseService(
isEnterpriseBuild = true, isEnterpriseBuild = true,
@ -203,12 +205,14 @@ class DeveloperSettingsPresenterTest {
assertThat(awaitItem().showColorPicker).isTrue() assertThat(awaitItem().showColorPicker).isTrue()
initialState.eventSink(DeveloperSettingsEvents.ChangeBrandColor(Color.Green)) initialState.eventSink(DeveloperSettingsEvents.ChangeBrandColor(Color.Green))
assertThat(awaitItem().showColorPicker).isFalse() assertThat(awaitItem().showColorPicker).isFalse()
skipItems(1)
overrideBrandColorResult.assertions().isCalledOnce() overrideBrandColorResult.assertions().isCalledOnce()
.with(value("00FF00")) .with(value(A_SESSION_ID), value("00FF00"))
} }
} }
private fun createDeveloperSettingsPresenter( private fun createDeveloperSettingsPresenter(
sessionId: SessionId = A_SESSION_ID,
featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService(
getAvailableFeaturesResult = { _, _ -> getAvailableFeaturesResult = { _, _ ->
listOf( listOf(
@ -227,6 +231,7 @@ class DeveloperSettingsPresenterTest {
enterpriseService: EnterpriseService = FakeEnterpriseService(), enterpriseService: EnterpriseService = FakeEnterpriseService(),
): DeveloperSettingsPresenter { ): DeveloperSettingsPresenter {
return DeveloperSettingsPresenter( return DeveloperSettingsPresenter(
sessionId = sessionId,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
computeCacheSizeUseCase = cacheSizeUseCase, computeCacheSizeUseCase = cacheSizeUseCase,
clearCacheUseCase = clearCacheUseCase, clearCacheUseCase = clearCacheUseCase,

View file

@ -36,14 +36,6 @@ interface BugReporter {
*/ */
fun logDirectory(): File 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. * Set the current tracing log level.
*/ */

View file

@ -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.data.tryOrNull
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.mimetype.MimeTypes 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.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.SdkMetadata 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.core.UserId
import io.element.android.libraries.matrix.api.tracing.TracingService import io.element.android.libraries.matrix.api.tracing.TracingService
import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.network.useragent.UserAgentProvider
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.api.sessionIdFlow
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first 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.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
@ -67,6 +73,8 @@ import java.util.Locale
@Inject @Inject
class DefaultBugReporter( class DefaultBugReporter(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val screenshotHolder: ScreenshotHolder, private val screenshotHolder: ScreenshotHolder,
private val crashDataStore: CrashDataStore, private val crashDataStore: CrashDataStore,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
@ -78,7 +86,6 @@ class DefaultBugReporter(
private val sdkMetadata: SdkMetadata, private val sdkMetadata: SdkMetadata,
private val matrixClientProvider: MatrixClientProvider, private val matrixClientProvider: MatrixClientProvider,
private val tracingService: TracingService, private val tracingService: TracingService,
matrixAuthenticationService: MatrixAuthenticationService,
) : BugReporter { ) : BugReporter {
companion object { companion object {
// filenames // filenames
@ -98,13 +105,18 @@ class DefaultBugReporter(
if (buildMeta.isEnterpriseBuild) { if (buildMeta.isEnterpriseBuild) {
val logSubfolder = runBlocking { val logSubfolder = runBlocking {
sessionStore.getLatestSession() sessionStore.getLatestSession()
}?.userId?.substringAfter(":") }?.userId?.let(::UserId)?.domainName
setCurrentLogDirectory(logSubfolder) setCurrentLogDirectory(logSubfolder)
matrixAuthenticationService.listenToNewMatrixClients { sessionStore.sessionIdFlow()
// When a new Matrix client is created, we update the tracing configuration to write .map {
// the files in a dedicated subfolders. it?.let(::UserId)?.domainName
setLogDirectorySubfolder(it.userIdServerName()) }
} .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?) { private fun setCurrentLogDirectory(subfolderName: String?) {
currentLogDirectory = if (subfolderName == null) { currentLogDirectory = if (subfolderName == null) {
baseLogDirectory baseLogDirectory

View file

@ -54,10 +54,6 @@ class FakeBugReporter(val mode: Mode = Mode.Success) : BugReporter {
return File("fake") return File("fake")
} }
override fun setLogDirectorySubfolder(subfolderName: String?) {
// No op
}
override fun setCurrentTracingLogLevel(logLevel: String) { override fun setCurrentTracingLogLevel(logLevel: String) {
// No op // No op
} }

View file

@ -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.features.rageshake.impl.screenshot.FakeScreenshotHolder
import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.MatrixClientProvider 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.TracingService
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import io.element.android.libraries.matrix.test.A_DEVICE_ID 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.FakeMatrixClient
import io.element.android.libraries.matrix.test.FakeMatrixClientProvider import io.element.android.libraries.matrix.test.FakeMatrixClientProvider
import io.element.android.libraries.matrix.test.FakeSdkMetadata 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.core.aBuildMeta
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService 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.libraries.sessionstorage.test.aSessionData
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okhttp3.MultipartReader import okhttp3.MultipartReader
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -405,53 +405,85 @@ class DefaultBugReporterTest {
assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs") assertThat(sut.logDirectory().absolutePath).endsWith("/cache/logs")
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test @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 var param: WriteToFilesConfiguration? = null
val updateWriteToFilesConfigurationResult = lambdaRecorder<WriteToFilesConfiguration, Unit> { val updateWriteToFilesConfigurationResult = lambdaRecorder<WriteToFilesConfiguration, Unit> {
param = it param = it
} }
val sut = createDefaultBugReporter( val sessionStore = InMemorySessionStore()
createDefaultBugReporter(
buildMeta = aBuildMeta(isEnterpriseBuild = true), buildMeta = aBuildMeta(isEnterpriseBuild = true),
sessionStore = sessionStore,
tracingService = FakeTracingService( tracingService = FakeTracingService(
updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult,
), ),
) )
sut.setLogDirectorySubfolder("my.sub.folder") sessionStore.addSession(aSessionData(sessionId = "@alice:server.org"))
runCurrent()
updateWriteToFilesConfigurationResult.assertions().isCalledOnce() updateWriteToFilesConfigurationResult.assertions().isCalledOnce()
assertThat(param).isNotNull() assertThat(param).isNotNull()
assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java) 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).filenamePrefix).isEqualTo("logs")
assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168) assertThat((param as WriteToFilesConfiguration.Enabled).numberOfFiles).isEqualTo(168)
assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log") assertThat((param as WriteToFilesConfiguration.Enabled).filenameSuffix).isEqualTo("log")
} }
@OptIn(ExperimentalCoroutinesApi::class)
@Test @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<WriteToFilesConfiguration, Unit> {} val updateWriteToFilesConfigurationResult = lambdaRecorder<WriteToFilesConfiguration, Unit> {}
val sut = createDefaultBugReporter( val sessionStore = InMemorySessionStore()
tracingService = FakeTracingService( createDefaultBugReporter(
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<WriteToFilesConfiguration, Unit> {
param = it
}
val sut = createDefaultBugReporter(
buildMeta = aBuildMeta(isEnterpriseBuild = true), buildMeta = aBuildMeta(isEnterpriseBuild = true),
sessionStore = sessionStore,
tracingService = FakeTracingService( tracingService = FakeTracingService(
updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, 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<WriteToFilesConfiguration, Unit> {}
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<WriteToFilesConfiguration, Unit> {
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() updateWriteToFilesConfigurationResult.assertions().isCalledOnce()
assertThat(param).isNotNull() assertThat(param).isNotNull()
assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java) assertThat(param).isInstanceOf(WriteToFilesConfiguration.Enabled::class.java)
@ -464,66 +496,16 @@ class DefaultBugReporterTest {
@Test @Test
fun `foss build - when the log directory is reset, the tracing service is not invoked`() = runTest { fun `foss build - when the log directory is reset, the tracing service is not invoked`() = runTest {
val updateWriteToFilesConfigurationResult = lambdaRecorder<WriteToFilesConfiguration, Unit> {} val updateWriteToFilesConfigurationResult = lambdaRecorder<WriteToFilesConfiguration, Unit> {}
val sut = createDefaultBugReporter( val sessionStore = InMemorySessionStore(
initialList = listOf(aSessionData(sessionId = "@alice:server.org")),
)
createDefaultBugReporter(
tracingService = FakeTracingService( tracingService = FakeTracingService(
updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult, updateWriteToFilesConfigurationResult = updateWriteToFilesConfigurationResult,
) ),
sessionStore = sessionStore,
) )
sut.setLogDirectorySubfolder(null) sessionStore.removeSession("@alice:server.org")
updateWriteToFilesConfigurationResult.assertions().isNeverCalled()
}
@Test
fun `when a new MatrixClient is created the logs folder is updated`() = runTest {
var param: WriteToFilesConfiguration? = null
val updateWriteToFilesConfigurationResult = lambdaRecorder<WriteToFilesConfiguration, Unit> {
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<WriteToFilesConfiguration, Unit> {}
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")
updateWriteToFilesConfigurationResult.assertions().isNeverCalled() updateWriteToFilesConfigurationResult.assertions().isNeverCalled()
} }
@ -534,10 +516,10 @@ class DefaultBugReporterTest {
crashDataStore: CrashDataStore = FakeCrashDataStore(), crashDataStore: CrashDataStore = FakeCrashDataStore(),
server: MockWebServer = MockWebServer(), server: MockWebServer = MockWebServer(),
tracingService: TracingService = FakeTracingService(), tracingService: TracingService = FakeTracingService(),
matrixAuthenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(),
): DefaultBugReporter { ): DefaultBugReporter {
return DefaultBugReporter( return DefaultBugReporter(
context = RuntimeEnvironment.getApplication(), context = RuntimeEnvironment.getApplication(),
appCoroutineScope = backgroundScope,
screenshotHolder = FakeScreenshotHolder(), screenshotHolder = FakeScreenshotHolder(),
crashDataStore = crashDataStore, crashDataStore = crashDataStore,
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),
@ -549,7 +531,6 @@ class DefaultBugReporterTest {
sdkMetadata = FakeSdkMetadata("123456789"), sdkMetadata = FakeSdkMetadata("123456789"),
matrixClientProvider = matrixClientProvider, matrixClientProvider = matrixClientProvider,
tracingService = tracingService, tracingService = tracingService,
matrixAuthenticationService = matrixAuthenticationService,
) )
} }

View file

@ -73,3 +73,15 @@ fun List<SessionData>.toUserList(): List<String> {
fun Flow<List<SessionData>>.toUserListFlow(): Flow<List<String>> { fun Flow<List<SessionData>>.toUserListFlow(): Flow<List<String>> {
return map { it.toUserList() } return map { it.toUserList() }
} }
/**
* @return a flow emitting the sessionId of the latest session if logged in, null otherwise.
*/
fun SessionStore.sessionIdFlow(): Flow<String?> {
return loggedInStateFlow().map {
when (it) {
is LoggedInState.LoggedIn -> it.sessionId
is LoggedInState.NotLoggedIn -> null
}
}
}

View file

@ -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.SessionData
import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.api.SessionStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
@ -47,6 +48,7 @@ class DatabaseSessionStore(
) )
} }
} }
.distinctUntilChanged()
} }
override suspend fun addSession(sessionData: SessionData) { override suspend fun addSession(sessionData: SessionData) {

View file

@ -11,4 +11,5 @@ data class ElementWellKnown(
val registrationHelperUrl: String?, val registrationHelperUrl: String?,
val enforceElementPro: Boolean?, val enforceElementPro: Boolean?,
val rageshakeUrl: String?, val rageshakeUrl: String?,
val brandColor: String?,
) )

View file

@ -27,4 +27,6 @@ data class InternalElementWellKnown(
val enforceElementPro: Boolean? = null, val enforceElementPro: Boolean? = null,
@SerialName("rageshake_url") @SerialName("rageshake_url")
val rageshakeUrl: String? = null, val rageshakeUrl: String? = null,
@SerialName("brand_color")
val brandColor: String? = null,
) )

View file

@ -15,6 +15,7 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl, registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro, enforceElementPro = enforceElementPro,
rageshakeUrl = rageshakeUrl, rageshakeUrl = rageshakeUrl,
brandColor = brandColor,
) )
internal fun InternalWellKnown.map() = WellKnown( internal fun InternalWellKnown.map() = WellKnown(

View file

@ -161,6 +161,7 @@ class DefaultSessionWellknownRetrieverTest {
registrationHelperUrl = null, registrationHelperUrl = null,
enforceElementPro = null, enforceElementPro = null,
rageshakeUrl = null, rageshakeUrl = null,
brandColor = null,
) )
) )
getUrlLambda.assertions().isCalledOnce() getUrlLambda.assertions().isCalledOnce()
@ -175,7 +176,8 @@ class DefaultSessionWellknownRetrieverTest {
"""{ """{
"registration_helper_url": "a_registration_url", "registration_helper_url": "a_registration_url",
"enforce_element_pro": true, "enforce_element_pro": true,
"rageshake_url": "a_rageshake_url" "rageshake_url": "a_rageshake_url",
"brand_color": "#FF0000"
}""".trimIndent().toByteArray() }""".trimIndent().toByteArray()
) )
} }
@ -185,6 +187,7 @@ class DefaultSessionWellknownRetrieverTest {
registrationHelperUrl = "a_registration_url", registrationHelperUrl = "a_registration_url",
enforceElementPro = true, enforceElementPro = true,
rageshakeUrl = "a_rageshake_url", rageshakeUrl = "a_rageshake_url",
brandColor = "#FF0000",
) )
) )
} }
@ -208,6 +211,7 @@ class DefaultSessionWellknownRetrieverTest {
registrationHelperUrl = "a_registration_url", registrationHelperUrl = "a_registration_url",
enforceElementPro = true, enforceElementPro = true,
rageshakeUrl = "a_rageshake_url", rageshakeUrl = "a_rageshake_url",
brandColor = null,
) )
) )
} }

View file

@ -13,8 +13,10 @@ fun anElementWellKnown(
registrationHelperUrl: String? = null, registrationHelperUrl: String? = null,
enforceElementPro: Boolean? = null, enforceElementPro: Boolean? = null,
rageshakeUrl: String? = null, rageshakeUrl: String? = null,
brandColor: String? = null,
) = ElementWellKnown( ) = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl, registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro, enforceElementPro = enforceElementPro,
rageshakeUrl = rageshakeUrl, rageshakeUrl = rageshakeUrl,
brandColor = brandColor,
) )