Move the :libraries:wellknown logic into enterprise modules (#7429)

* Extract `IsEnterpriseBuild` use case

Having it inside `EnterpriseService` made having DI cycles with the `BuildMeta` provider too easy

* Rename `EnterpriseService.defaultHomeserverList()` to `homeserverAllowList()`

This seems more aligned with the way this list works

* Move most of the logic from `:libraries:wellknown` to `:enterprise:feature:enterprise`, both `shared` and `impl` modules

Ideally I would have added everything into `impl`, but some of the classes needed to be shared between the different `:enterprise:features` modules, and those are in the `shared` module, lacking a better name/strategy

* Add `EnterpriseService.isElementProEnforced`

With this, we don't need to provide the `RemoteEnterpriseConfig` anymore to any consumers, everything goes through `EnterpriseService` or other providers

* Remove `EnterpriseService.overriddenElementWellKnown`

This leaks the remote enterprise config/element well known contents to the app in a public interface.

* Create `CustomMapTilerConfigProvider` to avoid leaking the `RemoteEnterpriseConfig` in public APIs

This commit also starts moving some of the well-known related values to the enterprise modules

* Add missing docs to `EnterpriseService` and `SessionEnterpriseService`

* Fix tests

* Fix lint issues

* Bump enterprise module
This commit is contained in:
Jorge Martin Espinosa
2026-08-17 15:21:47 +02:00
committed by GitHub
parent f94566e44f
commit 8b2c207728
72 changed files with 246 additions and 1607 deletions
@@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.Color
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
/**
@@ -21,9 +20,6 @@ import kotlinx.coroutines.flow.Flow
* On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves.
*/
interface EnterpriseService {
/** Whether this build is an enterprise one; `false` for the public app. */
val isEnterpriseBuild: Boolean
/**
* Whether the given session belongs to an enterprise deployment, which can be true even on a standard build.
*
@@ -41,17 +37,26 @@ interface EnterpriseService {
suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher): String
/**
* The homeservers the user is allowed to sign in to; an empty list, or one containing [ANY_ACCOUNT_PROVIDER], means no restriction.
* Returns the list of homeservers the user is allowed to sign in to.
*
* If the list is empty or contains the special value [ANY_ACCOUNT_PROVIDER], the user is allowed to sign in to any homeserver.
*/
fun defaultHomeserverList(): List<String>
fun homeserverAllowList(): List<String>
/**
* Whether the user is allowed to sign in to a given homeserver, according to [defaultHomeserverList].
* Whether the user is allowed to sign in to a given homeserver, according to [homeserverAllowList].
*
* @param homeserverUrl the server the user is trying to use.
*/
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
/**
* Whether the given homeserver enforces the use of Element Pro or a derived app.
*
* @param homeserverUrl the homeserver to check.
*/
suspend fun isElementProEnforced(homeserverUrl: String): Boolean
/**
* 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.
@@ -94,11 +99,6 @@ interface EnterpriseService {
*/
fun getNoisyNotificationChannelId(sessionId: SessionId): String?
/**
* Gets the overridden Element Well-Known data if it has been set, or null if not set.
*/
fun overriddenElementWellKnown(): ElementWellKnown?
/**
* Gets the Element Server Suite (ESS) config endpoint URL for the given domain.
* Returns `null` when this build does not read its configuration from an ESS deployment.
@@ -113,7 +113,7 @@ interface EnterpriseService {
}
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
return defaultHomeserverList().let {
return homeserverAllowList().let {
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
}
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.api
/**
* A use case to check if this build of the app is an enterprise build.
*/
fun interface IsEnterpriseBuild {
/**
* Returns `true` if this build of the app is an enterprise build, `false` otherwise.
*/
operator fun invoke(): Boolean
}
@@ -14,6 +14,9 @@ package io.element.android.features.enterprise.api
* [init] must have run before the other members return meaningful answers.
*/
interface SessionEnterpriseService {
/** Loads the deployment configuration of this session; to be called once when the session starts. */
suspend fun init()
/** Whether Element Call may be used on this session, which a deployment can turn off. */
suspend fun isElementCallAvailable(): Boolean
@@ -26,7 +29,4 @@ interface SessionEnterpriseService {
/** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */
suspend fun isEncryptionDisabledByHomeserver(): Boolean
/** Loads the deployment configuration of this session; to be called once when the session starts. */
suspend fun init()
}
@@ -0,0 +1,15 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.api.remoteconfig
/**
* A provider of a runtime-fetched MapTiler configuration, which can be used to override the default MapTiler configuration.
*/
fun interface CustomMapTilerConfigProvider {
suspend fun get(): Result<MapTilerConfig?>
}
@@ -0,0 +1,25 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.api.remoteconfig
import androidx.compose.runtime.Immutable
/**
* A `MapTiler` configuration received from the server. This configuration is used to display maps in the app.
*/
@Immutable
data class MapTilerConfig(
/** The API key to use with MapTiler. This is always not null, but can be empty/blank. */
val apiKey: String,
/** The style ID to use for light mode. This is optional and can be null. */
val lightStyleId: String?,
/** The style ID to use for light mode. This is optional and can be null. */
val darkStyleId: String?,
/** The base URL to use for MapTiler. This is optional and can be null. */
val baseUrl: String?,
)
@@ -1,17 +0,0 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.api.remoteconfig
import io.element.android.libraries.wellknown.api.ElementWellKnown
/**
* Typealias for the well-known configuration used for enterprise features.
*
* This will allow for easier refactoring in the future when we'll want to move as much of its logic as possible to this module.
*/
typealias RemoteEnterpriseConfig = ElementWellKnown
@@ -1,35 +0,0 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.api.remoteconfig
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
/**
* Wrapper to fetch the remote enterprise configuration for a given [SessionId], either from a local cache or from a remote source.
*/
interface RemoteEnterpriseConfigProvider {
/**
* Returns the configuration of a session, served from the cache when possible.
* A stale entry is reported as outdated while a refresh runs in the background, so this rarely waits on the network.
*
* @param sessionId the session whose configuration is requested.
*/
suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig>
/**
* Creates a provider, which needs a fetcher because the configuration is read over HTTP.
*/
interface Factory {
/**
* @param urlContentFetcher used to perform the request; a client that is not authenticated yet also works.
*/
fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.api.remoteconfig
sealed interface RemoteEnterpriseConfigResult<out T> {
/**
* Well-known data has been successfully retrieved.
*/
data class Success<out T>(val data: T) : RemoteEnterpriseConfigResult<T>
/**
* Well-known data has been retrieved from the local cache but is outdated and a manual refresh has been launched.
*/
data class Outdated<out T>(val data: T) : RemoteEnterpriseConfigResult<T>
/**
* Well-known data is not found (file does not exist server side, we got a 404).
*/
data object NotFound : RemoteEnterpriseConfigResult<Nothing>
/**
* Any other error.
*/
data class Error(val exception: Exception) : RemoteEnterpriseConfigResult<Nothing>
fun dataOrNull(): T? = when (this) {
is Success<T> -> data
is Outdated<T> -> data
is Error -> null
NotFound -> null
}
fun upToDateDataOrNull(): T? = when (this) {
is Success<T> -> data
is Outdated<T> -> null
is Error -> null
NotFound -> null
}
}
@@ -16,18 +16,16 @@ import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class)
class DefaultEnterpriseService : EnterpriseService {
override val isEnterpriseBuild = false
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher) = url
override fun defaultHomeserverList(): List<String> = emptyList()
override fun homeserverAllowList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = false
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
@@ -48,7 +46,5 @@ class DefaultEnterpriseService : EnterpriseService {
override fun getNoisyNotificationChannelId(sessionId: SessionId): String? = null
override fun overriddenElementWellKnown(): ElementWellKnown? = null
override fun essConfigEndpointUrl(domain: String): String? = null
}
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.impl
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.IsEnterpriseBuild
@ContributesBinding(AppScope::class)
class DefaultIsEnterpriseBuild : IsEnterpriseBuild {
override fun invoke(): Boolean = false
}
@@ -0,0 +1,18 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.impl.remoteconfig
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class)
class DefaultCustomMapTilerConfigProvider : CustomMapTilerConfigProvider {
override suspend fun get(): Result<MapTilerConfig?> = Result.success(null)
}
@@ -1,30 +0,0 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.impl.remoteconfig
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
@ContributesBinding(AppScope::class)
class DefaultRemoteEnterpriseConfigProvider : RemoteEnterpriseConfigProvider {
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
return WellknownRetrieverResult.NotFound
}
@ContributesBinding(AppScope::class)
class Factory : RemoteEnterpriseConfigProvider.Factory {
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider {
return DefaultRemoteEnterpriseConfigProvider()
}
}
}
@@ -19,15 +19,9 @@ import org.junit.Test
class DefaultEnterpriseServiceTest {
@Test
fun `isEnterpriseBuild is false`() {
fun homeserverWhitelist() {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse()
}
@Test
fun `defaultHomeserverList should return empty list`() {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
assertThat(defaultEnterpriseService.homeserverAllowList()).isEmpty()
}
@Test
@@ -14,7 +14,6 @@ import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.UrlContentFetcher
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.ElementWellKnown
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
@@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class FakeEnterpriseService(
override val isEnterpriseBuild: Boolean = false,
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
@@ -33,8 +31,8 @@ class FakeEnterpriseService(
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
private val overrideWellKnownResult: () -> ElementWellKnown? = { lambdaError() },
private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
private val isElementProEnforcedResult: (String) -> Boolean = { lambdaError() },
) : EnterpriseService {
private val brandColorState = MutableStateFlow(initialBrandColor)
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
@@ -47,7 +45,7 @@ class FakeEnterpriseService(
tweakMasUrlResult(url, homeserver, urlContentFetcher)
}
override fun defaultHomeserverList(): List<String> {
override fun homeserverAllowList(): List<String> {
return defaultHomeserverListResult()
}
@@ -55,6 +53,10 @@ class FakeEnterpriseService(
isAllowedToConnectToHomeserverResult(homeserverUrl)
}
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = simulateLongTask {
isElementProEnforcedResult(homeserverUrl)
}
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
overrideBrandColorResult(sessionId, brandColor)
}
@@ -84,10 +86,6 @@ class FakeEnterpriseService(
return getNoisyNotificationChannelIdResult(sessionId)
}
override fun overriddenElementWellKnown(): ElementWellKnown? {
return overrideWellKnownResult()
}
override fun essConfigEndpointUrl(domain: String): String {
return essConfigEndpointUrlResult(domain)
}
@@ -1,21 +0,0 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
class FakeRemoteEnterpriseConfigProvider(
private val getResult: (SessionId) -> WellknownRetrieverResult<RemoteEnterpriseConfig> = { WellknownRetrieverResult.NotFound }
) : RemoteEnterpriseConfigProvider {
override suspend fun get(sessionId: SessionId): WellknownRetrieverResult<RemoteEnterpriseConfig> {
return getResult(sessionId)
}
}
@@ -1,17 +0,0 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
import io.element.android.libraries.matrix.api.UrlContentFetcher
class FakeRemoteEnterpriseConfigProviderFactory(
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
) : RemoteEnterpriseConfigProvider.Factory {
override fun create(urlContentFetcher: UrlContentFetcher): RemoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider
}
@@ -0,0 +1,22 @@
/*
* Copyright (c) 2026 Element Creations 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.enterprise.test.remoteconfig
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
fun aMapTilerConfig(
apiKey: String = "test_api_key",
baseUrl: String? = null,
lightStyleId: String? = null,
darkStyleId: String? = null,
) = MapTilerConfig(
apiKey = apiKey,
baseUrl = baseUrl,
lightStyleId = lightStyleId,
darkStyleId = darkStyleId,
)