mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-05 22:36:34 +00:00
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:
committed by
GitHub
parent
f94566e44f
commit
8b2c207728
@@ -259,6 +259,7 @@ dependencies {
|
|||||||
if (isEnterpriseBuild) {
|
if (isEnterpriseBuild) {
|
||||||
allEnterpriseImpl(project)
|
allEnterpriseImpl(project)
|
||||||
implementation(projects.appicon.enterprise)
|
implementation(projects.appicon.enterprise)
|
||||||
|
implementation(project(":enterprise:features:enterprise:shared"))
|
||||||
} else {
|
} else {
|
||||||
implementation(projects.features.enterprise.implFoss)
|
implementation(projects.features.enterprise.implFoss)
|
||||||
implementation(projects.appicon.element)
|
implementation(projects.appicon.element)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import dev.zacsweers.metro.ContributesTo
|
|||||||
import dev.zacsweers.metro.Provides
|
import dev.zacsweers.metro.Provides
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.appconfig.ApplicationConfig
|
import io.element.android.appconfig.ApplicationConfig
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.IsEnterpriseBuild
|
||||||
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
|
import io.element.android.libraries.androidutils.system.getVersionCodeFromManifest
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
@@ -74,17 +74,18 @@ object AppModule {
|
|||||||
fun providesBuildMeta(
|
fun providesBuildMeta(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
buildType: BuildType,
|
buildType: BuildType,
|
||||||
enterpriseService: EnterpriseService,
|
isEnterpriseBuild: IsEnterpriseBuild,
|
||||||
): BuildMeta {
|
): BuildMeta {
|
||||||
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
|
val applicationName = ApplicationConfig.APPLICATION_NAME.takeIf { it.isNotEmpty() } ?: context.getString(R.string.app_name)
|
||||||
|
val isEnterprise = isEnterpriseBuild()
|
||||||
return BuildMeta(
|
return BuildMeta(
|
||||||
isDebuggable = BuildConfig.DEBUG,
|
isDebuggable = BuildConfig.DEBUG,
|
||||||
buildType = buildType,
|
buildType = buildType,
|
||||||
applicationName = applicationName,
|
applicationName = applicationName,
|
||||||
productionApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
|
productionApplicationName = if (isEnterprise) applicationName else ApplicationConfig.PRODUCTION_APPLICATION_NAME,
|
||||||
desktopApplicationName = if (enterpriseService.isEnterpriseBuild) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
|
desktopApplicationName = if (isEnterprise) applicationName else ApplicationConfig.DESKTOP_APPLICATION_NAME,
|
||||||
applicationId = BuildConfig.APPLICATION_ID,
|
applicationId = BuildConfig.APPLICATION_ID,
|
||||||
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
|
isEnterpriseBuild = isEnterprise,
|
||||||
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
|
// TODO EAx Config.LOW_PRIVACY_LOG_ENABLE,
|
||||||
lowPrivacyLoggingEnabled = false,
|
lowPrivacyLoggingEnabled = false,
|
||||||
versionName = BuildConfig.VERSION_NAME,
|
versionName = BuildConfig.VERSION_NAME,
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import io.element.android.compound.colors.SemanticColorsLightDark
|
|||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
import io.element.android.features.enterprise.api.SessionEnterpriseService
|
||||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
@@ -157,7 +157,7 @@ class LoggedInFlowNode(
|
|||||||
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
|
private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher,
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
private val activeLiveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||||
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = NavTarget.Placeholder,
|
initialElement = NavTarget.Placeholder,
|
||||||
@@ -685,7 +685,7 @@ class LoggedInFlowNode(
|
|||||||
|
|
||||||
val currentMapTilerConfig = LocalMapTilerConfig.current
|
val currentMapTilerConfig = LocalMapTilerConfig.current
|
||||||
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
|
val updatedMapTilerConfig by produceState(currentMapTilerConfig) {
|
||||||
value = remoteEnterpriseConfigProvider.get(matrixClient.sessionId).dataOrNull()?.mapTilerConfig ?: currentMapTilerConfig
|
value = customMapTilerConfigProvider.get().getOrNull() ?: currentMapTilerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
ElementThemeApp(
|
ElementThemeApp(
|
||||||
|
|||||||
+1
-1
Submodule enterprise updated: f5f910856c...3bf0df1f25
+13
-13
@@ -12,7 +12,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import io.element.android.compound.colors.SemanticColorsLightDark
|
import io.element.android.compound.colors.SemanticColorsLightDark
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
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.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.
|
* On a standard build this reports permissive defaults, so callers do not need to branch on the build type themselves.
|
||||||
*/
|
*/
|
||||||
interface EnterpriseService {
|
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.
|
* 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
|
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.
|
* @param homeserverUrl the server the user is trying to use.
|
||||||
*/
|
*/
|
||||||
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean
|
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.
|
* 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 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?
|
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.
|
* 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.
|
* Returns `null` when this build does not read its configuration from an ESS deployment.
|
||||||
@@ -113,7 +113,7 @@ interface EnterpriseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
|
fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
|
||||||
return defaultHomeserverList().let {
|
return homeserverAllowList().let {
|
||||||
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
|
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
@@ -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
|
||||||
|
}
|
||||||
+3
-3
@@ -14,6 +14,9 @@ package io.element.android.features.enterprise.api
|
|||||||
* [init] must have run before the other members return meaningful answers.
|
* [init] must have run before the other members return meaningful answers.
|
||||||
*/
|
*/
|
||||||
interface SessionEnterpriseService {
|
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. */
|
/** Whether Element Call may be used on this session, which a deployment can turn off. */
|
||||||
suspend fun isElementCallAvailable(): Boolean
|
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. */
|
/** Whether the homeserver forbids creating encrypted rooms, in which case the app must not offer the option. */
|
||||||
suspend fun isEncryptionDisabledByHomeserver(): Boolean
|
suspend fun isEncryptionDisabledByHomeserver(): Boolean
|
||||||
|
|
||||||
/** Loads the deployment configuration of this session; to be called once when the session starts. */
|
|
||||||
suspend fun init()
|
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -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?>
|
||||||
|
}
|
||||||
+4
-15
@@ -1,29 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
* Copyright 2023-2025 New Vector Ltd.
|
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.api
|
package io.element.android.features.enterprise.api.remoteconfig
|
||||||
|
|
||||||
data class ElementWellKnown(
|
import androidx.compose.runtime.Immutable
|
||||||
val registrationHelperUrl: String?,
|
|
||||||
val enforceElementPro: Boolean?,
|
|
||||||
val rageshakeUrl: String?,
|
|
||||||
val brandColor: String?,
|
|
||||||
val notificationSound: String?,
|
|
||||||
val identityProviderAppScheme: String?,
|
|
||||||
val customRecoveryPassphrase: CustomRecoveryPassphrase?,
|
|
||||||
val contentScannerUrl: String?,
|
|
||||||
val forceDisableE2EE: Boolean?,
|
|
||||||
val mapTilerConfig: MapTilerConfig?,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A `MapTiler` configuration received from the server. This configuration is used to display maps in the app.
|
* A `MapTiler` configuration received from the server. This configuration is used to display maps in the app.
|
||||||
*/
|
*/
|
||||||
|
@Immutable
|
||||||
data class MapTilerConfig(
|
data class MapTilerConfig(
|
||||||
/** The API key to use with MapTiler. This is always not null, but can be empty/blank. */
|
/** The API key to use with MapTiler. This is always not null, but can be empty/blank. */
|
||||||
val apiKey: String,
|
val apiKey: String,
|
||||||
-17
@@ -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
|
|
||||||
-35
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+7
-8
@@ -1,33 +1,32 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.api
|
package io.element.android.features.enterprise.api.remoteconfig
|
||||||
|
|
||||||
sealed interface WellknownRetrieverResult<out T> {
|
sealed interface RemoteEnterpriseConfigResult<out T> {
|
||||||
/**
|
/**
|
||||||
* Well-known data has been successfully retrieved.
|
* Well-known data has been successfully retrieved.
|
||||||
*/
|
*/
|
||||||
data class Success<out T>(val data: T) : WellknownRetrieverResult<T>
|
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.
|
* 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) : WellknownRetrieverResult<T>
|
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).
|
* Well-known data is not found (file does not exist server side, we got a 404).
|
||||||
*/
|
*/
|
||||||
data object NotFound : WellknownRetrieverResult<Nothing>
|
data object NotFound : RemoteEnterpriseConfigResult<Nothing>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any other error.
|
* Any other error.
|
||||||
*/
|
*/
|
||||||
data class Error(val exception: Exception) : WellknownRetrieverResult<Nothing>
|
data class Error(val exception: Exception) : RemoteEnterpriseConfigResult<Nothing>
|
||||||
|
|
||||||
fun dataOrNull(): T? = when (this) {
|
fun dataOrNull(): T? = when (this) {
|
||||||
is Success<T> -> data
|
is Success<T> -> data
|
||||||
+2
-6
@@ -16,18 +16,16 @@ import io.element.android.features.enterprise.api.BugReportUrl
|
|||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
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.Flow
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultEnterpriseService : EnterpriseService {
|
class DefaultEnterpriseService : EnterpriseService {
|
||||||
override val isEnterpriseBuild = false
|
|
||||||
|
|
||||||
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
|
override suspend fun isEnterpriseUser(sessionId: SessionId) = false
|
||||||
override suspend fun tweakMasUrl(url: String, homeserver: String, urlContentFetcher: UrlContentFetcher) = url
|
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 isAllowedToConnectToHomeserver(homeserverUrl: String) = true
|
||||||
|
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = false
|
||||||
|
|
||||||
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
|
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 getNoisyNotificationChannelId(sessionId: SessionId): String? = null
|
||||||
|
|
||||||
override fun overriddenElementWellKnown(): ElementWellKnown? = null
|
|
||||||
|
|
||||||
override fun essConfigEndpointUrl(domain: String): String? = null
|
override fun essConfigEndpointUrl(domain: String): String? = null
|
||||||
}
|
}
|
||||||
|
|||||||
+17
@@ -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
|
||||||
|
}
|
||||||
+18
@@ -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)
|
||||||
|
}
|
||||||
-30
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-8
@@ -19,15 +19,9 @@ import org.junit.Test
|
|||||||
|
|
||||||
class DefaultEnterpriseServiceTest {
|
class DefaultEnterpriseServiceTest {
|
||||||
@Test
|
@Test
|
||||||
fun `isEnterpriseBuild is false`() {
|
fun homeserverWhitelist() {
|
||||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
val defaultEnterpriseService = DefaultEnterpriseService()
|
||||||
assertThat(defaultEnterpriseService.isEnterpriseBuild).isFalse()
|
assertThat(defaultEnterpriseService.homeserverAllowList()).isEmpty()
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `defaultHomeserverList should return empty list`() {
|
|
||||||
val defaultEnterpriseService = DefaultEnterpriseService()
|
|
||||||
assertThat(defaultEnterpriseService.defaultHomeserverList()).isEmpty()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+6
-8
@@ -14,7 +14,6 @@ import io.element.android.features.enterprise.api.BugReportUrl
|
|||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
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.lambda.lambdaError
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -22,7 +21,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
class FakeEnterpriseService(
|
class FakeEnterpriseService(
|
||||||
override val isEnterpriseBuild: Boolean = false,
|
|
||||||
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
|
||||||
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
|
||||||
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
||||||
@@ -33,8 +31,8 @@ class FakeEnterpriseService(
|
|||||||
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
|
||||||
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
|
||||||
private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
|
private val tweakMasUrlResult: (String, String, UrlContentFetcher) -> String = { _, _, _ -> lambdaError() },
|
||||||
private val overrideWellKnownResult: () -> ElementWellKnown? = { lambdaError() },
|
|
||||||
private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
|
private val essConfigEndpointUrlResult: (String) -> String = { lambdaError() },
|
||||||
|
private val isElementProEnforcedResult: (String) -> Boolean = { lambdaError() },
|
||||||
) : EnterpriseService {
|
) : EnterpriseService {
|
||||||
private val brandColorState = MutableStateFlow(initialBrandColor)
|
private val brandColorState = MutableStateFlow(initialBrandColor)
|
||||||
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
|
||||||
@@ -47,7 +45,7 @@ class FakeEnterpriseService(
|
|||||||
tweakMasUrlResult(url, homeserver, urlContentFetcher)
|
tweakMasUrlResult(url, homeserver, urlContentFetcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun defaultHomeserverList(): List<String> {
|
override fun homeserverAllowList(): List<String> {
|
||||||
return defaultHomeserverListResult()
|
return defaultHomeserverListResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +53,10 @@ class FakeEnterpriseService(
|
|||||||
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
isAllowedToConnectToHomeserverResult(homeserverUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun isElementProEnforced(homeserverUrl: String): Boolean = simulateLongTask {
|
||||||
|
isElementProEnforcedResult(homeserverUrl)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
|
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = simulateLongTask {
|
||||||
overrideBrandColorResult(sessionId, brandColor)
|
overrideBrandColorResult(sessionId, brandColor)
|
||||||
}
|
}
|
||||||
@@ -84,10 +86,6 @@ class FakeEnterpriseService(
|
|||||||
return getNoisyNotificationChannelIdResult(sessionId)
|
return getNoisyNotificationChannelIdResult(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun overriddenElementWellKnown(): ElementWellKnown? {
|
|
||||||
return overrideWellKnownResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun essConfigEndpointUrl(domain: String): String {
|
override fun essConfigEndpointUrl(domain: String): String {
|
||||||
return essConfigEndpointUrlResult(domain)
|
return essConfigEndpointUrlResult(domain)
|
||||||
}
|
}
|
||||||
|
|||||||
-21
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -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
|
|
||||||
}
|
|
||||||
+22
@@ -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,
|
||||||
|
)
|
||||||
@@ -71,6 +71,7 @@ dependencies {
|
|||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.wellknown.api)
|
implementation(projects.libraries.wellknown.api)
|
||||||
|
implementation(projects.features.enterprise.api)
|
||||||
implementation(libs.coil.compose)
|
implementation(libs.coil.compose)
|
||||||
implementation(libs.datetime)
|
implementation(libs.datetime)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
package io.element.android.features.location.api
|
package io.element.android.features.location.api
|
||||||
|
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides a [MapTilerConfig] for the MapLibre maps.
|
* Provides a [MapTilerConfig] for the MapLibre maps.
|
||||||
|
|||||||
+1
-1
@@ -37,6 +37,7 @@ import coil3.compose.rememberAsyncImagePainter
|
|||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.SuccessResult
|
import coil3.request.SuccessResult
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
import io.element.android.features.location.api.internal.StaticMapPlaceholder
|
||||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
import io.element.android.features.location.api.internal.centerBottomEdge
|
||||||
import io.element.android.features.location.api.internal.rememberStaticMapBuilder
|
import io.element.android.features.location.api.internal.rememberStaticMapBuilder
|
||||||
@@ -45,7 +46,6 @@ import io.element.android.libraries.designsystem.components.PinVariant
|
|||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a static map image downloaded via a third party service's static maps API.
|
* Shows a static map image downloaded via a third party service's static maps API.
|
||||||
|
|||||||
+1
-1
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
package io.element.android.features.location.api.internal
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.location.api.BuildConfig
|
import io.element.android.features.location.api.BuildConfig
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
package io.element.android.features.location.api.internal
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.location.api.BuildConfig
|
import io.element.android.features.location.api.BuildConfig
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
|
|
||||||
internal class MapTilerTileServerStyleUriBuilder(
|
internal class MapTilerTileServerStyleUriBuilder(
|
||||||
private val baseUrl: String,
|
private val baseUrl: String,
|
||||||
|
|||||||
+1
-2
@@ -10,8 +10,7 @@ package io.element.android.features.location.api.internal
|
|||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds an URL for a 3rd party service provider static maps API.
|
* Builds an URL for a 3rd party service provider static maps API.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ package io.element.android.features.location.api.internal
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a style URI for a MapLibre compatible tile server.
|
* Builds a style URI for a MapLibre compatible tile server.
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@
|
|||||||
package io.element.android.features.location.api.internal
|
package io.element.android.features.location.api.internal
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
class MapTilerTileServerStyleUriBuilderTest {
|
class MapTilerTileServerStyleUriBuilderTest {
|
||||||
|
|||||||
+1
-1
@@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.max
|
import androidx.compose.ui.unit.max
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
import io.element.android.features.location.api.internal.rememberTileStyleUrl
|
||||||
import io.element.android.features.location.impl.common.MapDefaults
|
import io.element.android.features.location.impl.common.MapDefaults
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
@@ -50,7 +51,6 @@ import io.element.android.libraries.core.data.tryOrNull
|
|||||||
import io.element.android.libraries.designsystem.text.toDp
|
import io.element.android.libraries.designsystem.text.toDp
|
||||||
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
|
import io.element.android.libraries.designsystem.theme.components.BottomSheetScaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
import org.maplibre.compose.camera.CameraState
|
import org.maplibre.compose.camera.CameraState
|
||||||
import org.maplibre.compose.camera.rememberCameraState
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
import org.maplibre.compose.map.MapOptions
|
import org.maplibre.compose.map.MapOptions
|
||||||
|
|||||||
+3
-3
@@ -22,7 +22,7 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||||
import io.element.android.features.location.impl.common.LocationConstraintsCheck
|
import io.element.android.features.location.impl.common.LocationConstraintsCheck
|
||||||
import io.element.android.features.location.impl.common.MapDefaults
|
import io.element.android.features.location.impl.common.MapDefaults
|
||||||
@@ -73,7 +73,7 @@ class ShareLocationPresenter(
|
|||||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
private val liveLocationStore: LiveLocationStore,
|
private val liveLocationStore: LiveLocationStore,
|
||||||
private val userLocationStateFactory: UserLocationState.Factory,
|
private val userLocationStateFactory: UserLocationState.Factory,
|
||||||
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||||
) : Presenter<ShareLocationState> {
|
) : Presenter<ShareLocationState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
fun interface Factory {
|
fun interface Factory {
|
||||||
@@ -96,7 +96,7 @@ class ShareLocationPresenter(
|
|||||||
val currentUser by client.userProfile.collectAsState()
|
val currentUser by client.userProfile.collectAsState()
|
||||||
val customMapConfig by produceState(AsyncData.Loading()) {
|
val customMapConfig by produceState(AsyncData.Loading()) {
|
||||||
// Ignore errors
|
// Ignore errors
|
||||||
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(client.sessionId).dataOrNull()?.mapTilerConfig)
|
value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull())
|
||||||
}
|
}
|
||||||
val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms ->
|
val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms ->
|
||||||
perms.sendLiveLocationPermissions()
|
perms.sendLiveLocationPermissions()
|
||||||
|
|||||||
+1
-1
@@ -8,12 +8,12 @@
|
|||||||
|
|
||||||
package io.element.android.features.location.impl.share
|
package io.element.android.features.location.impl.share
|
||||||
|
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ShareLocationState(
|
data class ShareLocationState(
|
||||||
|
|||||||
+1
-1
@@ -9,13 +9,13 @@
|
|||||||
package io.element.android.features.location.impl.share
|
package io.element.android.features.location.impl.share
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
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.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlin.time.Duration.Companion.hours
|
import kotlin.time.Duration.Companion.hours
|
||||||
import kotlin.time.Duration.Companion.minutes
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|||||||
+5
-6
@@ -22,7 +22,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import dev.zacsweers.metro.Assisted
|
import dev.zacsweers.metro.Assisted
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
import io.element.android.features.location.api.ShowLocationMode
|
||||||
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
import io.element.android.features.location.api.live.ActiveLiveLocationShareManager
|
||||||
@@ -47,7 +47,6 @@ import io.element.android.libraries.dateformatter.api.DateFormatter
|
|||||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.getBestName
|
import io.element.android.libraries.matrix.api.room.getBestName
|
||||||
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
|
||||||
@@ -58,18 +57,18 @@ import kotlinx.collections.immutable.toImmutableList
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AssistedInject class ShowLocationPresenter(
|
@AssistedInject
|
||||||
|
class ShowLocationPresenter(
|
||||||
@Assisted private val mode: ShowLocationMode,
|
@Assisted private val mode: ShowLocationMode,
|
||||||
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
permissionsPresenterFactory: PermissionsPresenter.Factory,
|
||||||
private val locationActions: LocationActions,
|
private val locationActions: LocationActions,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val dateFormatter: DateFormatter,
|
private val dateFormatter: DateFormatter,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val sessionId: SessionId,
|
|
||||||
private val joinedRoom: JoinedRoom,
|
private val joinedRoom: JoinedRoom,
|
||||||
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
private val liveLocationShareManager: ActiveLiveLocationShareManager,
|
||||||
private val userLocationStateFactory: UserLocationState.Factory,
|
private val userLocationStateFactory: UserLocationState.Factory,
|
||||||
private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider,
|
private val customMapTilerConfigProvider: CustomMapTilerConfigProvider,
|
||||||
) : Presenter<ShowLocationState> {
|
) : Presenter<ShowLocationState> {
|
||||||
@AssistedFactory fun interface Factory {
|
@AssistedFactory fun interface Factory {
|
||||||
fun create(mode: ShowLocationMode): ShowLocationPresenter
|
fun create(mode: ShowLocationMode): ShowLocationPresenter
|
||||||
@@ -89,7 +88,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
val customMapTilerConfig by produceState(AsyncData.Loading()) {
|
val customMapTilerConfig by produceState(AsyncData.Loading()) {
|
||||||
// Ignore errors
|
// Ignore errors
|
||||||
value = AsyncData.Success(remoteEnterpriseConfigProvider.get(sessionId).dataOrNull()?.mapTilerConfig)
|
value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkLocationConstraints() {
|
fun checkLocationConstraints() {
|
||||||
|
|||||||
+1
-1
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
package io.element.android.features.location.impl.show
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||||
import io.element.android.features.location.impl.common.ui.LocationMarkerData
|
import io.element.android.features.location.impl.common.ui.LocationMarkerData
|
||||||
@@ -17,7 +18,6 @@ import io.element.android.libraries.designsystem.components.PinVariant
|
|||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
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.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ShowLocationState(
|
data class ShowLocationState(
|
||||||
|
|||||||
+1
-1
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.features.location.impl.show
|
package io.element.android.features.location.impl.show
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||||
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
import io.element.android.features.location.impl.common.userlocation.UserLocationState
|
||||||
@@ -18,7 +19,6 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
|||||||
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||||
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.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
||||||
|
|||||||
+1
-2
@@ -11,7 +11,6 @@ package io.element.android.features.location.impl.share
|
|||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
|
||||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||||
@@ -59,7 +58,7 @@ class DefaultShareLocationEntryPointTest {
|
|||||||
sessionId = room.sessionId,
|
sessionId = room.sessionId,
|
||||||
),
|
),
|
||||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
customMapTilerConfigProvider = { Result.success(null) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
|||||||
+5
-10
@@ -19,8 +19,8 @@ import app.cash.turbine.ReceiveTurbine
|
|||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
import im.vector.app.features.analytics.plan.Composer
|
||||||
import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider
|
import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
|
||||||
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.impl.aPermissionsState
|
import io.element.android.features.location.impl.aPermissionsState
|
||||||
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
import io.element.android.features.location.impl.common.FakeUserLocationStateFactory
|
||||||
@@ -32,8 +32,6 @@ import io.element.android.features.location.impl.common.ui.LocationConstraintsDi
|
|||||||
import io.element.android.features.location.impl.live.LiveLocationStore
|
import io.element.android.features.location.impl.live.LiveLocationStore
|
||||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
import io.element.android.features.wellknown.test.aMapTilerConfig
|
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDurationFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDurationFormatter
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
@@ -55,7 +53,6 @@ import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermiss
|
|||||||
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
import io.element.android.libraries.matrix.test.timeline.FakeTimeline
|
||||||
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory
|
||||||
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
|
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
@@ -92,7 +89,7 @@ class ShareLocationPresenterTest {
|
|||||||
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||||
liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId),
|
liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId),
|
||||||
client: FakeMatrixClient = fakeMatrixClient,
|
client: FakeMatrixClient = fakeMatrixClient,
|
||||||
remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
customMapTilerConfigProvider: () -> Result<MapTilerConfig?> = { Result.success(null) },
|
||||||
): ShareLocationPresenter = ShareLocationPresenter(
|
): ShareLocationPresenter = ShareLocationPresenter(
|
||||||
permissionsPresenterFactory = { fakePermissionsPresenter },
|
permissionsPresenterFactory = { fakePermissionsPresenter },
|
||||||
room = joinedRoom,
|
room = joinedRoom,
|
||||||
@@ -106,7 +103,7 @@ class ShareLocationPresenterTest {
|
|||||||
liveLocationShareManager = liveLocationShareManager,
|
liveLocationShareManager = liveLocationShareManager,
|
||||||
liveLocationStore = liveLocationStore,
|
liveLocationStore = liveLocationStore,
|
||||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||||
remoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider,
|
customMapTilerConfigProvider = customMapTilerConfigProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -134,9 +131,7 @@ class ShareLocationPresenterTest {
|
|||||||
client = FakeMatrixClient(
|
client = FakeMatrixClient(
|
||||||
sessionId = A_USER_ID,
|
sessionId = A_USER_ID,
|
||||||
),
|
),
|
||||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
|
customMapTilerConfigProvider = { Result.success(mapTilerConfig) }
|
||||||
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = mapTilerConfig)) }
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
shareLocationPresenter.test {
|
shareLocationPresenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
|
|||||||
+1
-4
@@ -11,7 +11,6 @@ package io.element.android.features.location.impl.show
|
|||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
import io.element.android.features.location.api.ShowLocationMode
|
||||||
@@ -21,7 +20,6 @@ import io.element.android.features.location.impl.common.permissions.FakePermissi
|
|||||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
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.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
@@ -51,10 +49,9 @@ class DefaultShowLocationEntryPointTest {
|
|||||||
dateFormatter = FakeDateFormatter(),
|
dateFormatter = FakeDateFormatter(),
|
||||||
stringProvider = FakeStringProvider(),
|
stringProvider = FakeStringProvider(),
|
||||||
joinedRoom = joinedRoom,
|
joinedRoom = joinedRoom,
|
||||||
sessionId = A_SESSION_ID,
|
|
||||||
liveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
liveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
customMapTilerConfigProvider = { Result.success(null) },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
|||||||
+6
-22
@@ -12,7 +12,8 @@ import app.cash.molecule.RecompositionMode
|
|||||||
import app.cash.molecule.moleculeFlow
|
import app.cash.molecule.moleculeFlow
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider
|
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
|
||||||
|
import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
import io.element.android.features.location.api.ShowLocationMode
|
||||||
import io.element.android.features.location.impl.aPermissionsState
|
import io.element.android.features.location.impl.aPermissionsState
|
||||||
@@ -23,20 +24,15 @@ import io.element.android.features.location.impl.common.permissions.PermissionsE
|
|||||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||||
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
import io.element.android.features.location.test.FakeActiveLiveLocationShareManager
|
||||||
import io.element.android.features.wellknown.test.aMapTilerConfig
|
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
|
||||||
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
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.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||||
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.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare
|
import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
import io.element.android.services.toolbox.test.strings.FakeStringProvider
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
@@ -69,9 +65,8 @@ class ShowLocationPresenterTest {
|
|||||||
),
|
),
|
||||||
locationActions: FakeLocationActions = fakeLocationActions,
|
locationActions: FakeLocationActions = fakeLocationActions,
|
||||||
joinedRoom: JoinedRoom = FakeJoinedRoom(),
|
joinedRoom: JoinedRoom = FakeJoinedRoom(),
|
||||||
sessionId: SessionId = A_SESSION_ID,
|
|
||||||
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(),
|
||||||
remoteEnterpriseConfigProvider: FakeRemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(),
|
customMapTilerConfigProvider: CustomMapTilerConfigProvider = { Result.success(null) },
|
||||||
) = ShowLocationPresenter(
|
) = ShowLocationPresenter(
|
||||||
mode = mode,
|
mode = mode,
|
||||||
permissionsPresenterFactory = { fakePermissionsPresenter },
|
permissionsPresenterFactory = { fakePermissionsPresenter },
|
||||||
@@ -80,21 +75,15 @@ class ShowLocationPresenterTest {
|
|||||||
dateFormatter = fakeDateFormatter,
|
dateFormatter = fakeDateFormatter,
|
||||||
stringProvider = FakeStringProvider(),
|
stringProvider = FakeStringProvider(),
|
||||||
joinedRoom = joinedRoom,
|
joinedRoom = joinedRoom,
|
||||||
sessionId = sessionId,
|
|
||||||
liveLocationShareManager = liveLocationShareManager,
|
liveLocationShareManager = liveLocationShareManager,
|
||||||
userLocationStateFactory = FakeUserLocationStateFactory(),
|
userLocationStateFactory = FakeUserLocationStateFactory(),
|
||||||
remoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider,
|
customMapTilerConfigProvider = customMapTilerConfigProvider,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - non-null customMapStyleUrl`() = runTest {
|
fun `present - non-null customMapStyleUrl`() = runTest {
|
||||||
val mapTilerConfig = aMapTilerConfig(apiKey = "A KEY")
|
val mapTilerConfig = aMapTilerConfig(apiKey = "A KEY")
|
||||||
val shareLocationPresenter = createShowLocationPresenter(
|
val shareLocationPresenter = createShowLocationPresenter(customMapTilerConfigProvider = { Result.success(mapTilerConfig) })
|
||||||
sessionId = A_SESSION_ID,
|
|
||||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
|
|
||||||
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = mapTilerConfig)) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
shareLocationPresenter.test {
|
shareLocationPresenter.test {
|
||||||
val state = awaitItem()
|
val state = awaitItem()
|
||||||
assertThat(state.customMapTilerConfig.isLoading()).isTrue()
|
assertThat(state.customMapTilerConfig.isLoading()).isTrue()
|
||||||
@@ -131,12 +120,7 @@ class ShowLocationPresenterTest {
|
|||||||
fun `centers on user location`() = runTest {
|
fun `centers on user location`() = runTest {
|
||||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
val presenter = createShowLocationPresenter(
|
val presenter = createShowLocationPresenter(customMapTilerConfigProvider = { Result.success(aMapTilerConfig()) })
|
||||||
sessionId = A_SESSION_ID,
|
|
||||||
remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(
|
|
||||||
getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = aMapTilerConfig())) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
presenter.test {
|
presenter.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
|
|||||||
+5
-18
@@ -11,17 +11,14 @@ package io.element.android.features.login.impl.accesscontrol
|
|||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.api.IsEnterpriseBuild
|
||||||
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
import io.element.android.features.login.api.accesscontrol.AccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||||
import io.element.android.libraries.matrix.api.TemporaryMatrixClientFactory
|
|
||||||
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultAccountProviderAccessControl(
|
class DefaultAccountProviderAccessControl(
|
||||||
|
private val isEnterpriseBuild: IsEnterpriseBuild,
|
||||||
private val enterpriseService: EnterpriseService,
|
private val enterpriseService: EnterpriseService,
|
||||||
private val wellknownRetrieverFactory: WellknownRetriever.Factory,
|
|
||||||
private val temporaryMatrixClientFactory: TemporaryMatrixClientFactory,
|
|
||||||
) : AccountProviderAccessControl {
|
) : AccountProviderAccessControl {
|
||||||
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
|
override suspend fun isAllowedToConnectToAccountProvider(accountProviderUrl: String) = try {
|
||||||
assertIsAllowedToConnectToAccountProvider(
|
assertIsAllowedToConnectToAccountProvider(
|
||||||
@@ -38,29 +35,19 @@ class DefaultAccountProviderAccessControl(
|
|||||||
title: String,
|
title: String,
|
||||||
accountProviderUrl: String,
|
accountProviderUrl: String,
|
||||||
) {
|
) {
|
||||||
if (enterpriseService.isEnterpriseBuild.not()) {
|
if (isEnterpriseBuild().not()) {
|
||||||
// Ensure that Element Pro is not required for this account provider
|
// Ensure that Element Pro is not required for this account provider
|
||||||
val temporaryMatrixClient = temporaryMatrixClientFactory.create(accountProviderUrl).getOrThrow()
|
if (enterpriseService.isElementProEnforced(accountProviderUrl)) {
|
||||||
temporaryMatrixClient.use {
|
|
||||||
val wellknownRetriever = wellknownRetrieverFactory.create(temporaryMatrixClient)
|
|
||||||
val wellKnown = wellknownRetriever.getElementWellKnown(
|
|
||||||
host = accountProviderUrl,
|
|
||||||
// It's expected this is hardcoded to the well-known endpoint
|
|
||||||
source = EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT,
|
|
||||||
).dataOrNull()
|
|
||||||
|
|
||||||
if (wellKnown?.enforceElementPro == true) {
|
|
||||||
throw AccountProviderAccessException.NeedElementProException(
|
throw AccountProviderAccessException.NeedElementProException(
|
||||||
unauthorisedAccountProviderTitle = title,
|
unauthorisedAccountProviderTitle = title,
|
||||||
applicationId = ELEMENT_PRO_APPLICATION_ID,
|
applicationId = ELEMENT_PRO_APPLICATION_ID,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) {
|
if (enterpriseService.isAllowedToConnectToHomeserver(accountProviderUrl).not()) {
|
||||||
throw AccountProviderAccessException.UnauthorizedAccountProviderException(
|
throw AccountProviderAccessException.UnauthorizedAccountProviderException(
|
||||||
unauthorisedAccountProviderTitle = title,
|
unauthorisedAccountProviderTitle = title,
|
||||||
authorisedAccountProviderTitles = enterpriseService.defaultHomeserverList(),
|
authorisedAccountProviderTitles = enterpriseService.homeserverAllowList(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ class AccountProviderDataSource(
|
|||||||
enterpriseService: EnterpriseService,
|
enterpriseService: EnterpriseService,
|
||||||
) {
|
) {
|
||||||
private val defaultAccountProvider = createAccountProvider(
|
private val defaultAccountProvider = createAccountProvider(
|
||||||
url = enterpriseService.defaultHomeserverList()
|
url = enterpriseService.homeserverAllowList()
|
||||||
.firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
.firstOrNull { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
||||||
?: AuthenticationConfig.MATRIX_ORG_URL
|
?: AuthenticationConfig.MATRIX_ORG_URL
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ class ChangeAccountProviderPresenter(
|
|||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChangeAccountProviderState {
|
override fun present(): ChangeAccountProviderState {
|
||||||
val staticAccountProviderList = remember {
|
val staticAccountProviderList = remember {
|
||||||
enterpriseService.defaultHomeserverList()
|
enterpriseService.homeserverAllowList()
|
||||||
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
.filter { it != EnterpriseService.ANY_ACCOUNT_PROVIDER }
|
||||||
.map { it.ensureProtocol() }
|
.map { it.ensureProtocol() }
|
||||||
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
.ifEmpty { listOf(AuthenticationConfig.MATRIX_ORG_URL) }
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ class ChooseAccountProviderPresenter(
|
|||||||
|
|
||||||
val staticAccountProviderList = remember {
|
val staticAccountProviderList = remember {
|
||||||
// The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point
|
// The list cannot contains ANY_ACCOUNT_PROVIDER ("*") and cannot be empty at this point
|
||||||
enterpriseService.defaultHomeserverList()
|
enterpriseService.homeserverAllowList()
|
||||||
.map { it.ensureProtocol() }
|
.map { it.ensureProtocol() }
|
||||||
.map { url ->
|
.map { url ->
|
||||||
AccountProvider(
|
AccountProvider(
|
||||||
|
|||||||
+2
-2
@@ -62,13 +62,13 @@ class OnBoardingPresenter(
|
|||||||
val forcedAccountProvider = remember {
|
val forcedAccountProvider = remember {
|
||||||
// If defaultHomeserverList() returns a singleton list, this is the default account provider.
|
// If defaultHomeserverList() returns a singleton list, this is the default account provider.
|
||||||
// In this case, the user can sign in using this homeserver, or use QrCode login
|
// In this case, the user can sign in using this homeserver, or use QrCode login
|
||||||
enterpriseService.defaultHomeserverList().singleOrNull()
|
enterpriseService.homeserverAllowList().singleOrNull()
|
||||||
}
|
}
|
||||||
val canConnectToAnyHomeserver = remember {
|
val canConnectToAnyHomeserver = remember {
|
||||||
enterpriseService.canConnectToAnyHomeserver()
|
enterpriseService.canConnectToAnyHomeserver()
|
||||||
}
|
}
|
||||||
val mustChooseAccountProvider = remember {
|
val mustChooseAccountProvider = remember {
|
||||||
!canConnectToAnyHomeserver && enterpriseService.defaultHomeserverList().size > 1
|
!canConnectToAnyHomeserver && enterpriseService.homeserverAllowList().size > 1
|
||||||
}
|
}
|
||||||
val linkAccountProvider by produceState<String?>(initialValue = null) {
|
val linkAccountProvider by produceState<String?>(initialValue = null) {
|
||||||
// Account provider from the link, if allowed by the enterprise service
|
// Account provider from the link, if allowed by the enterprise service
|
||||||
|
|||||||
+30
-46
@@ -11,15 +11,9 @@ package io.element.android.features.login.impl.accesscontrol
|
|||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
import io.element.android.features.login.impl.changeserver.AccountProviderAccessException
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
||||||
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertThrows
|
import org.junit.Assert.assertThrows
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
@@ -30,9 +24,7 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = anElementWellKnown(
|
enterpriseService = FakeEnterpriseService(isElementProEnforcedResult = { true }),
|
||||||
enforceElementPro = true,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
accessControl.expectNeedElementProException()
|
accessControl.expectNeedElementProException()
|
||||||
}
|
}
|
||||||
@@ -43,9 +35,7 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
// false here.
|
// false here.
|
||||||
isAllowedToConnectToHomeserver = false,
|
isAllowedToConnectToHomeserver = false,
|
||||||
elementWellKnown = anElementWellKnown(
|
enterpriseService = FakeEnterpriseService(isElementProEnforcedResult = { true }),
|
||||||
enforceElementPro = true,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
accessControl.expectNeedElementProException()
|
accessControl.expectNeedElementProException()
|
||||||
}
|
}
|
||||||
@@ -55,8 +45,9 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = anElementWellKnown(
|
enterpriseService = FakeEnterpriseService(
|
||||||
enforceElementPro = false,
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
isElementProEnforcedResult = { false }
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
accessControl.expectAllowed()
|
accessControl.expectAllowed()
|
||||||
@@ -67,19 +58,15 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = anElementWellKnown(
|
|
||||||
enforceElementPro = null,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
accessControl.expectAllowed()
|
accessControl.expectAllowed()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `foss build should allow using account provider twith missing wellknown`() = runTest {
|
fun `foss build should allow using account provider with missing wellknown`() = runTest {
|
||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = null,
|
|
||||||
)
|
)
|
||||||
accessControl.expectAllowed()
|
accessControl.expectAllowed()
|
||||||
}
|
}
|
||||||
@@ -90,8 +77,10 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
isEnterpriseBuild = false,
|
isEnterpriseBuild = false,
|
||||||
isAllowedToConnectToHomeserver = false,
|
isAllowedToConnectToHomeserver = false,
|
||||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||||
elementWellKnown = anElementWellKnown(
|
enterpriseService = FakeEnterpriseService(
|
||||||
enforceElementPro = false,
|
isAllowedToConnectToHomeserverResult = { false },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
|
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
accessControl.expectUnauthorizedAccountProviderException()
|
accessControl.expectUnauthorizedAccountProviderException()
|
||||||
@@ -102,8 +91,9 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = true,
|
isEnterpriseBuild = true,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = anElementWellKnown(
|
enterpriseService = FakeEnterpriseService(
|
||||||
enforceElementPro = true,
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
isElementProEnforcedResult = { true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
accessControl.expectAllowed()
|
accessControl.expectAllowed()
|
||||||
@@ -114,8 +104,9 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
val accessControl = createDefaultAccountProviderAccessControl(
|
val accessControl = createDefaultAccountProviderAccessControl(
|
||||||
isEnterpriseBuild = true,
|
isEnterpriseBuild = true,
|
||||||
isAllowedToConnectToHomeserver = true,
|
isAllowedToConnectToHomeserver = true,
|
||||||
elementWellKnown = anElementWellKnown(
|
enterpriseService = FakeEnterpriseService(
|
||||||
enforceElementPro = false,
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
accessControl.expectAllowed()
|
accessControl.expectAllowed()
|
||||||
@@ -127,8 +118,10 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
isEnterpriseBuild = true,
|
isEnterpriseBuild = true,
|
||||||
isAllowedToConnectToHomeserver = false,
|
isAllowedToConnectToHomeserver = false,
|
||||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||||
elementWellKnown = anElementWellKnown(
|
enterpriseService = FakeEnterpriseService(
|
||||||
enforceElementPro = true,
|
isAllowedToConnectToHomeserverResult = { false },
|
||||||
|
isElementProEnforcedResult = { true },
|
||||||
|
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
accessControl.expectUnauthorizedAccountProviderException()
|
accessControl.expectUnauthorizedAccountProviderException()
|
||||||
@@ -140,8 +133,10 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
isEnterpriseBuild = true,
|
isEnterpriseBuild = true,
|
||||||
isAllowedToConnectToHomeserver = false,
|
isAllowedToConnectToHomeserver = false,
|
||||||
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
allowedAccountProviders = listOf(AN_ACCOUNT_PROVIDER_2),
|
||||||
elementWellKnown = anElementWellKnown(
|
enterpriseService = FakeEnterpriseService(
|
||||||
enforceElementPro = false,
|
isAllowedToConnectToHomeserverResult = { false },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
|
defaultHomeserverListResult = { listOf(AN_ACCOUNT_PROVIDER_2) },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
accessControl.expectUnauthorizedAccountProviderException()
|
accessControl.expectUnauthorizedAccountProviderException()
|
||||||
@@ -151,25 +146,14 @@ class DefaultAccountProviderAccessControlTest {
|
|||||||
isEnterpriseBuild: Boolean = false,
|
isEnterpriseBuild: Boolean = false,
|
||||||
isAllowedToConnectToHomeserver: Boolean = false,
|
isAllowedToConnectToHomeserver: Boolean = false,
|
||||||
allowedAccountProviders: List<String> = emptyList(),
|
allowedAccountProviders: List<String> = emptyList(),
|
||||||
elementWellKnown: ElementWellKnown? = null,
|
enterpriseService: FakeEnterpriseService = FakeEnterpriseService(
|
||||||
) = DefaultAccountProviderAccessControl(
|
|
||||||
enterpriseService = FakeEnterpriseService(
|
|
||||||
isEnterpriseBuild = isEnterpriseBuild,
|
|
||||||
isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver },
|
isAllowedToConnectToHomeserverResult = { isAllowedToConnectToHomeserver },
|
||||||
defaultHomeserverListResult = { allowedAccountProviders },
|
defaultHomeserverListResult = { allowedAccountProviders },
|
||||||
),
|
isElementProEnforcedResult = { false },
|
||||||
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(
|
)
|
||||||
wellknownRetriever = FakeWellknownRetriever(
|
) = DefaultAccountProviderAccessControl(
|
||||||
getElementWellKnownResult = { _, _ ->
|
isEnterpriseBuild = { isEnterpriseBuild },
|
||||||
if (elementWellKnown == null) {
|
enterpriseService = enterpriseService,
|
||||||
WellknownRetrieverResult.NotFound
|
|
||||||
} else {
|
|
||||||
WellknownRetrieverResult.Success(elementWellKnown)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun DefaultAccountProviderAccessControl.expectNeedElementProException() {
|
private fun DefaultAccountProviderAccessControl.expectNeedElementProException() {
|
||||||
|
|||||||
+15
-23
@@ -16,22 +16,15 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider
|
|||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.error.ChangeServerError
|
import io.element.android.features.login.impl.error.ChangeServerError
|
||||||
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails
|
||||||
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog
|
||||||
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenter
|
||||||
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory
|
||||||
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
@@ -64,6 +57,7 @@ class ChangeServerPresenterTest {
|
|||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult = { true },
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
),
|
),
|
||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
@@ -86,6 +80,7 @@ class ChangeServerPresenterTest {
|
|||||||
createPresenter(
|
createPresenter(
|
||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult = { true },
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
),
|
),
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
).test {
|
).test {
|
||||||
@@ -113,6 +108,7 @@ class ChangeServerPresenterTest {
|
|||||||
createPresenter(
|
createPresenter(
|
||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult = { true },
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
),
|
),
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
).test {
|
).test {
|
||||||
@@ -136,6 +132,7 @@ class ChangeServerPresenterTest {
|
|||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult,
|
isAllowedToConnectToHomeserverResult = isAllowedToConnectToHomeserverResult,
|
||||||
defaultHomeserverListResult = { listOf("element.io") },
|
defaultHomeserverListResult = { listOf("element.io") },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
),
|
),
|
||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
@@ -159,23 +156,19 @@ class ChangeServerPresenterTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change server element pro required error`() = runTest {
|
fun `present - change server element pro required error`() = runTest {
|
||||||
val getElementWellKnownResult = lambdaRecorder { homeserver: String, _: EnterpriseRemoteConfigSource ->
|
val isEnterpriseBuildLambda = lambdaRecorder<Boolean> { false }
|
||||||
assertThat(homeserver).isEqualTo(A_HOMESERVER_URL)
|
|
||||||
WellknownRetrieverResult.Success(
|
|
||||||
anElementWellKnown(
|
|
||||||
enforceElementPro = true,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
createPresenter(
|
createPresenter(
|
||||||
wellknownRetriever = FakeWellknownRetriever(
|
isEnterpriseBuild = isEnterpriseBuildLambda,
|
||||||
getElementWellKnownResult = getElementWellKnownResult,
|
enterpriseService = FakeEnterpriseService(
|
||||||
),
|
isElementProEnforcedResult = { true },
|
||||||
|
)
|
||||||
).test {
|
).test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized)
|
||||||
val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL)
|
val anAccountProvider = AccountProvider(url = A_HOMESERVER_URL)
|
||||||
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider))
|
initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(anAccountProvider))
|
||||||
|
// Skip loading state
|
||||||
|
skipItems(1)
|
||||||
val failureState = awaitItem()
|
val failureState = awaitItem()
|
||||||
assertThat(
|
assertThat(
|
||||||
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).unauthorisedAccountProviderTitle
|
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).unauthorisedAccountProviderTitle
|
||||||
@@ -183,7 +176,7 @@ class ChangeServerPresenterTest {
|
|||||||
assertThat(
|
assertThat(
|
||||||
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId
|
(failureState.changeServerAction.errorOrNull() as ChangeServerError.NeedElementPro).applicationId
|
||||||
).isEqualTo("io.element.enterprise")
|
).isEqualTo("io.element.enterprise")
|
||||||
getElementWellKnownResult.assertions().isCalledOnce()
|
isEnterpriseBuildLambda.assertions().isCalledOnce()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +230,7 @@ class ChangeServerPresenterTest {
|
|||||||
val permissionsPresenter = FakePermissionsPresenter()
|
val permissionsPresenter = FakePermissionsPresenter()
|
||||||
createPresenter(
|
createPresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }),
|
enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }, isElementProEnforcedResult = { false }),
|
||||||
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true),
|
||||||
permissionsPresenter = permissionsPresenter,
|
permissionsPresenter = permissionsPresenter,
|
||||||
).test {
|
).test {
|
||||||
@@ -259,16 +252,15 @@ class ChangeServerPresenterTest {
|
|||||||
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(),
|
||||||
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
|
||||||
localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(),
|
||||||
permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(),
|
permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(),
|
||||||
|
isEnterpriseBuild: () -> Boolean = { false },
|
||||||
) = ChangeServerPresenter(
|
) = ChangeServerPresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
accountProviderDataSource = accountProviderDataSource,
|
accountProviderDataSource = accountProviderDataSource,
|
||||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(wellknownRetriever = wellknownRetriever),
|
isEnterpriseBuild = isEnterpriseBuild,
|
||||||
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
|
||||||
),
|
),
|
||||||
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
localNetworkPermissionGate = LocalNetworkPermissionGate(
|
||||||
advisor = localNetworkPermissionAdvisor,
|
advisor = localNetworkPermissionAdvisor,
|
||||||
|
|||||||
+5
-7
@@ -12,13 +12,12 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.appconfig.AuthenticationConfig
|
import io.element.android.appconfig.AuthenticationConfig
|
||||||
import io.element.android.appconfig.OnBoardingConfig
|
import io.element.android.appconfig.OnBoardingConfig
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
import io.element.android.features.enterprise.api.EnterpriseService
|
||||||
|
import io.element.android.features.enterprise.api.IsEnterpriseBuild
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
||||||
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
|
||||||
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate
|
||||||
import io.element.android.features.login.impl.login.LoginModePresenter
|
import io.element.android.features.login.impl.login.LoginModePresenter
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetriever
|
|
||||||
import io.element.android.features.wellknown.test.FakeWellknownRetrieverFactory
|
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
|
||||||
@@ -29,7 +28,6 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION
|
|||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL_2
|
||||||
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
import io.element.android.libraries.matrix.test.A_LOGIN_HINT
|
||||||
import io.element.android.libraries.matrix.test.FakeTemporaryMatrixClientFactory
|
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
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.oauth.api.OAuthActionFlow
|
import io.element.android.libraries.oauth.api.OAuthActionFlow
|
||||||
@@ -41,7 +39,6 @@ import io.element.android.libraries.permissions.test.FakePermissionsPresenterFac
|
|||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
|
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.libraries.wellknown.api.WellknownRetriever
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.test
|
import io.element.android.tests.testutils.test
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -186,6 +183,7 @@ class OnBoardingPresenterTest {
|
|||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
|
defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG, EnterpriseService.ANY_ACCOUNT_PROVIDER) },
|
||||||
isAllowedToConnectToHomeserverResult = { true },
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
@@ -259,6 +257,7 @@ class OnBoardingPresenterTest {
|
|||||||
),
|
),
|
||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(
|
||||||
isAllowedToConnectToHomeserverResult = { true },
|
isAllowedToConnectToHomeserverResult = { true },
|
||||||
|
isElementProEnforcedResult = { false },
|
||||||
),
|
),
|
||||||
loginModePresenter = createLoginModePresenter(
|
loginModePresenter = createLoginModePresenter(
|
||||||
authenticationService = authenticationService,
|
authenticationService = authenticationService,
|
||||||
@@ -295,7 +294,7 @@ private fun createPresenter(
|
|||||||
),
|
),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
enterpriseService: EnterpriseService = FakeEnterpriseService(),
|
||||||
wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
isEnterpriseBuild: IsEnterpriseBuild = { false },
|
||||||
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
|
rageshakeFeatureAvailability: () -> Flow<Boolean> = { flowOf(true) },
|
||||||
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
|
loginModePresenter: LoginModePresenter = createLoginModePresenter(),
|
||||||
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
|
onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null },
|
||||||
@@ -307,8 +306,7 @@ private fun createPresenter(
|
|||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
defaultAccountProviderAccessControl = DefaultAccountProviderAccessControl(
|
||||||
enterpriseService = enterpriseService,
|
enterpriseService = enterpriseService,
|
||||||
wellknownRetrieverFactory = FakeWellknownRetrieverFactory(wellknownRetriever = wellknownRetriever),
|
isEnterpriseBuild = isEnterpriseBuild,
|
||||||
temporaryMatrixClientFactory = FakeTemporaryMatrixClientFactory(),
|
|
||||||
),
|
),
|
||||||
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
rageshakeFeatureAvailability = rageshakeFeatureAvailability,
|
||||||
loginModePresenter = loginModePresenter,
|
loginModePresenter = loginModePresenter,
|
||||||
|
|||||||
+3
-1
@@ -30,6 +30,7 @@ import io.element.android.libraries.architecture.AsyncData
|
|||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.core.data.ByteUnit
|
import io.element.android.libraries.core.data.ByteUnit
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
||||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
@@ -50,6 +51,7 @@ class DeveloperSettingsPresenter(
|
|||||||
private val databaseSizesUseCase: GetDatabaseSizesUseCase,
|
private val databaseSizesUseCase: GetDatabaseSizesUseCase,
|
||||||
private val fileSizeFormatter: FileSizeFormatter,
|
private val fileSizeFormatter: FileSizeFormatter,
|
||||||
private val markAllRoomsAsRead: MarkAllRoomsAsRead,
|
private val markAllRoomsAsRead: MarkAllRoomsAsRead,
|
||||||
|
private val buildMeta: BuildMeta,
|
||||||
) : Presenter<DeveloperSettingsState> {
|
) : Presenter<DeveloperSettingsState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): DeveloperSettingsState {
|
override fun present(): DeveloperSettingsState {
|
||||||
@@ -117,7 +119,7 @@ class DeveloperSettingsPresenter(
|
|||||||
databaseSizes = databaseSizes.value,
|
databaseSizes = databaseSizes.value,
|
||||||
clearCacheAction = clearCacheAction.value,
|
clearCacheAction = clearCacheAction.value,
|
||||||
markAllRoomsAsReadAction = markAllRoomsAsReadAction.value,
|
markAllRoomsAsReadAction = markAllRoomsAsReadAction.value,
|
||||||
isEnterpriseBuild = enterpriseService.isEnterpriseBuild,
|
isEnterpriseBuild = buildMeta.isEnterpriseBuild,
|
||||||
showColorPicker = showColorPicker,
|
showColorPicker = showColorPicker,
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
|
|||||||
+6
-4
@@ -23,12 +23,14 @@ import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
|
|||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.data.megaBytes
|
import io.element.android.libraries.core.data.megaBytes
|
||||||
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
import io.element.android.libraries.matrix.api.analytics.GetDatabaseSizesUseCase
|
||||||
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
|
import io.element.android.libraries.matrix.api.analytics.SdkStoreSizes
|
||||||
import io.element.android.libraries.matrix.api.core.DeviceId
|
import io.element.android.libraries.matrix.api.core.DeviceId
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
@@ -108,10 +110,8 @@ class DeveloperSettingsPresenterTest {
|
|||||||
fun `present - enterprise build can change the brand color`() = runTest {
|
fun `present - enterprise build can change the brand color`() = runTest {
|
||||||
val overrideBrandColorResult = lambdaRecorder<SessionId?, String?, Unit> { _, _ -> }
|
val overrideBrandColorResult = lambdaRecorder<SessionId?, String?, Unit> { _, _ -> }
|
||||||
val presenter = createDeveloperSettingsPresenter(
|
val presenter = createDeveloperSettingsPresenter(
|
||||||
enterpriseService = FakeEnterpriseService(
|
enterpriseService = FakeEnterpriseService(overrideBrandColorResult = overrideBrandColorResult),
|
||||||
isEnterpriseBuild = true,
|
buildMeta = aBuildMeta(isEnterpriseBuild = true),
|
||||||
overrideBrandColorResult = overrideBrandColorResult,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
@@ -179,6 +179,7 @@ class DeveloperSettingsPresenterTest {
|
|||||||
vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {},
|
vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {},
|
||||||
databaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
|
databaseSizesUseCase: GetDatabaseSizesUseCase = GetDatabaseSizesUseCase { Result.success(SdkStoreSizes(null, null, null, null)) },
|
||||||
markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(),
|
markAllRoomsAsRead: FakeMarkAllRoomsAsRead = FakeMarkAllRoomsAsRead(),
|
||||||
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
): DeveloperSettingsPresenter {
|
): DeveloperSettingsPresenter {
|
||||||
return DeveloperSettingsPresenter(
|
return DeveloperSettingsPresenter(
|
||||||
appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() },
|
appDeveloperSettingsPresenter = { anAppDeveloperSettingsState() },
|
||||||
@@ -191,6 +192,7 @@ class DeveloperSettingsPresenterTest {
|
|||||||
databaseSizesUseCase = databaseSizesUseCase,
|
databaseSizesUseCase = databaseSizesUseCase,
|
||||||
fileSizeFormatter = FakeFileSizeFormatter(),
|
fileSizeFormatter = FakeFileSizeFormatter(),
|
||||||
markAllRoomsAsRead = markAllRoomsAsRead,
|
markAllRoomsAsRead = markAllRoomsAsRead,
|
||||||
|
buildMeta = buildMeta,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-23
@@ -1,23 +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.libraries.wellknown.api
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Server-driven settings for a user-chosen recovery passphrase, advertised under the well-known
|
|
||||||
* `custom_recovery_passphrase` key. Today the only rule is a minimum character count; additional
|
|
||||||
* settings can be added here as the schema grows.
|
|
||||||
*/
|
|
||||||
data class CustomRecoveryPassphrase(
|
|
||||||
val minCharacterCount: Int,
|
|
||||||
) {
|
|
||||||
/** True when [input] meets every active rule. */
|
|
||||||
fun isSatisfiedBy(input: String): Boolean = isSatisfiedBy(input.length)
|
|
||||||
|
|
||||||
/** True when an input of [length] characters meets every active rule. */
|
|
||||||
fun isSatisfiedBy(length: Int): Boolean = length >= minCharacterCount
|
|
||||||
}
|
|
||||||
-20
@@ -1,20 +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.libraries.wellknown.api
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses the raw JSON of an Element `.well-known` document.
|
|
||||||
*/
|
|
||||||
fun interface ElementWellKnownParser {
|
|
||||||
/**
|
|
||||||
* Parses the document, failing when it is not valid JSON; unknown fields are ignored so a newer server does not break an older client.
|
|
||||||
*
|
|
||||||
* @param json the raw document as served by the server.
|
|
||||||
*/
|
|
||||||
fun parse(json: String): Result<ElementWellKnown>
|
|
||||||
}
|
|
||||||
-47
@@ -1,47 +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.libraries.wellknown.api
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On-disk cache of the Element `.well-known` documents already fetched, so the app has a configuration to work with while offline.
|
|
||||||
*/
|
|
||||||
interface ElementWellknownStore {
|
|
||||||
/**
|
|
||||||
* Reads the cached document of a domain.
|
|
||||||
* Entries older than a day are reported as outdated rather than missing, so the caller can use them while refreshing;
|
|
||||||
* a document that no longer parses is reported as an error.
|
|
||||||
*
|
|
||||||
* @param domain the server whose cached document is requested.
|
|
||||||
*/
|
|
||||||
suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the document of a domain and stamps it as fetched now, replacing any previous entry.
|
|
||||||
*
|
|
||||||
* @param domain the server the document belongs to.
|
|
||||||
* @param wellknown the raw document, stored as-is rather than parsed.
|
|
||||||
*/
|
|
||||||
suspend fun update(domain: String, wellknown: String): Result<Unit>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes the cached document of a domain, used when the stored value turns out to be unusable.
|
|
||||||
*
|
|
||||||
* @param domain the server whose entry is removed.
|
|
||||||
*/
|
|
||||||
suspend fun delete(domain: String): Result<Unit>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a store, one per configuration source so that their entries never collide.
|
|
||||||
*/
|
|
||||||
fun interface Factory {
|
|
||||||
/**
|
|
||||||
* @param prefix namespaces the cache keys, or `null` for the default well-known endpoint.
|
|
||||||
*/
|
|
||||||
fun create(prefix: String?): ElementWellknownStore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-23
@@ -1,23 +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.libraries.wellknown.api
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The source from which the enterprise remote configuration is retrieved.
|
|
||||||
*/
|
|
||||||
enum class EnterpriseRemoteConfigSource {
|
|
||||||
/**
|
|
||||||
* Retrieve the enterprise remote configuration from the Element Server Suite configuration (ESS_CONFIG) endpoint.
|
|
||||||
*/
|
|
||||||
ESS_CONFIG,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the enterprise remote configuration from the Element server's well-known endpoint.
|
|
||||||
*/
|
|
||||||
WELLKNOWN_ENDPOINT,
|
|
||||||
}
|
|
||||||
-39
@@ -1,39 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.api
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the Element specific `.well-known` configuration of a server, which is how a deployment advertises its own settings.
|
|
||||||
*/
|
|
||||||
interface WellknownRetriever {
|
|
||||||
/**
|
|
||||||
* Returns the configuration of a host, served from the local cache when possible.
|
|
||||||
* A cached value that has gone stale is returned as outdated while a refresh runs in the background, so this rarely waits on the network.
|
|
||||||
* An enterprise build with a hardcoded configuration always short-circuits and returns that instead.
|
|
||||||
*
|
|
||||||
* @param host the server to look up; a full URL is accepted and reduced to its host.
|
|
||||||
* @param source which endpoint to read the configuration from, which also selects the cache it is stored in.
|
|
||||||
*/
|
|
||||||
suspend fun getElementWellKnown(
|
|
||||||
host: String,
|
|
||||||
source: EnterpriseRemoteConfigSource
|
|
||||||
): WellknownRetrieverResult<ElementWellKnown>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a retriever, 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): WellknownRetriever
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-27
@@ -1,27 +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.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
|
||||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
|
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
class DefaultElementWellKnownParser(
|
|
||||||
private val jsonProvider: JsonProvider,
|
|
||||||
) : ElementWellKnownParser {
|
|
||||||
override fun parse(json: String): Result<ElementWellKnown> {
|
|
||||||
return runCatchingExceptions {
|
|
||||||
val decoder = jsonProvider()
|
|
||||||
decoder.decodeFromString<InternalElementWellKnown>(json).map()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-77
@@ -1,77 +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.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
|
||||||
import dev.zacsweers.metro.Assisted
|
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
|
||||||
import dev.zacsweers.metro.AssistedInject
|
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
|
||||||
import io.element.android.libraries.cachestore.api.CacheData
|
|
||||||
import io.element.android.libraries.cachestore.api.CacheStore
|
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellknownStore
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import io.element.android.services.toolbox.api.systemclock.SystemClock
|
|
||||||
|
|
||||||
@AssistedInject
|
|
||||||
class DefaultWellKnownStore(
|
|
||||||
@Assisted
|
|
||||||
private val prefix: String?,
|
|
||||||
private val cacheStore: CacheStore,
|
|
||||||
private val elementWellKnownParser: ElementWellKnownParser,
|
|
||||||
private val systemClock: SystemClock,
|
|
||||||
) : ElementWellknownStore {
|
|
||||||
@AssistedFactory
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
fun interface Factory : ElementWellknownStore.Factory {
|
|
||||||
override fun create(prefix: String?): DefaultWellKnownStore
|
|
||||||
}
|
|
||||||
override suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown> {
|
|
||||||
return runCatchingExceptions {
|
|
||||||
val cachedData = cacheStore.getData(key(domain))
|
|
||||||
if (cachedData != null) {
|
|
||||||
val data = elementWellKnownParser.parse(cachedData.value).getOrThrow()
|
|
||||||
if (systemClock.epochMillis() > cachedData.updatedAt + CACHE_VALIDITY_MILLIS) {
|
|
||||||
WellknownRetrieverResult.Outdated(data)
|
|
||||||
} else {
|
|
||||||
WellknownRetrieverResult.Success(data)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
WellknownRetrieverResult.NotFound
|
|
||||||
}
|
|
||||||
}.recover {
|
|
||||||
WellknownRetrieverResult.Error(it as Exception)
|
|
||||||
}.getOrThrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun update(domain: String, wellknown: String): Result<Unit> {
|
|
||||||
return runCatchingExceptions {
|
|
||||||
cacheStore.storeData(key(domain), CacheData(wellknown, systemClock.epochMillis()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(domain: String): Result<Unit> {
|
|
||||||
return runCatchingExceptions {
|
|
||||||
cacheStore.deleteData(key(domain))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun key(domain: String): String = if (prefix != null) {
|
|
||||||
"$prefix-$domain"
|
|
||||||
} else {
|
|
||||||
"https://$domain/.well-known/element/element.json"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// 1 day
|
|
||||||
private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-142
@@ -1,142 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
|
||||||
import dev.zacsweers.metro.Assisted
|
|
||||||
import dev.zacsweers.metro.AssistedFactory
|
|
||||||
import dev.zacsweers.metro.AssistedInject
|
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
|
||||||
import io.element.android.libraries.androidutils.text.takeIfNotBlank
|
|
||||||
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
|
||||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellknownStore
|
|
||||||
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
@AssistedInject
|
|
||||||
class DefaultWellknownRetriever(
|
|
||||||
private val elementWellknownStoreFactory: ElementWellknownStore.Factory,
|
|
||||||
private val enterpriseService: EnterpriseService,
|
|
||||||
private val elementWellKnownParser: ElementWellKnownParser,
|
|
||||||
@Assisted private val urlContentFetcher: UrlContentFetcher,
|
|
||||||
@AppCoroutineScope private val coroutineScope: CoroutineScope,
|
|
||||||
) : WellknownRetriever {
|
|
||||||
@ContributesBinding(AppScope::class)
|
|
||||||
@AssistedFactory
|
|
||||||
fun interface Factory : WellknownRetriever.Factory {
|
|
||||||
override fun create(urlContentFetcher: UrlContentFetcher): DefaultWellknownRetriever
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getElementWellKnown(
|
|
||||||
host: String,
|
|
||||||
source: EnterpriseRemoteConfigSource
|
|
||||||
): WellknownRetrieverResult<ElementWellKnown> {
|
|
||||||
val overriddenElementWellKnown = enterpriseService.overriddenElementWellKnown()
|
|
||||||
if (overriddenElementWellKnown != null) {
|
|
||||||
return WellknownRetrieverResult.Success(overriddenElementWellKnown)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The passed `host` function may be a full URL, in that case we're only interested in the actual host
|
|
||||||
val checkedHost = runCatchingExceptions { URL(host.ensureProtocol()).host.takeIfNotBlank() }
|
|
||||||
.getOrElse { return WellknownRetrieverResult.Error(IllegalArgumentException("host parameter is not valid", it)) }
|
|
||||||
?: host
|
|
||||||
|
|
||||||
// We instantiate different stores for different sources, so that we can cache them separately.
|
|
||||||
// The ESS_CONFIG source is cached with a prefix to differentiate it from the WELLKNOWN_ENDPOINT.
|
|
||||||
val prefix = when (source) {
|
|
||||||
EnterpriseRemoteConfigSource.ESS_CONFIG -> "ess_config"
|
|
||||||
EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT -> null
|
|
||||||
}
|
|
||||||
val store = elementWellknownStoreFactory.create(prefix)
|
|
||||||
return when (val cacheData = store.get(checkedHost)) {
|
|
||||||
is WellknownRetrieverResult.Success -> {
|
|
||||||
Timber.d("Using cached well-known for domain $checkedHost")
|
|
||||||
cacheData
|
|
||||||
}
|
|
||||||
is WellknownRetrieverResult.Outdated -> {
|
|
||||||
// Return the outdated data but refresh in the background
|
|
||||||
// If the cache is missing or outdated, trigger a refresh in background but still return the cached value
|
|
||||||
Timber.d("Outdated cached well-known for domain $checkedHost, returning existing value and fetching new one from network")
|
|
||||||
coroutineScope.launch { fetchElementWellKnown(checkedHost, source, store) }
|
|
||||||
cacheData
|
|
||||||
}
|
|
||||||
is WellknownRetrieverResult.NotFound -> {
|
|
||||||
// Try to fetch from the server
|
|
||||||
Timber.d("No cached well-known for domain $checkedHost, fetching from network")
|
|
||||||
fetchElementWellKnown(checkedHost, source, store)
|
|
||||||
}
|
|
||||||
is WellknownRetrieverResult.Error -> {
|
|
||||||
// Return the error
|
|
||||||
Timber.e(cacheData.exception, "Error retrieving well-known for domain $checkedHost")
|
|
||||||
cacheData.exception.toWellknownRetrieverResult()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun fetchElementWellKnown(
|
|
||||||
host: String,
|
|
||||||
source: EnterpriseRemoteConfigSource,
|
|
||||||
store: ElementWellknownStore
|
|
||||||
): WellknownRetrieverResult<ElementWellKnown> {
|
|
||||||
return urlContentFetcher
|
|
||||||
.getUrl(url(host, source))
|
|
||||||
.mapCatchingExceptions {
|
|
||||||
val data = String(it)
|
|
||||||
val parsed = elementWellKnownParser.parse(data).getOrThrow()
|
|
||||||
// Also store in cache, if valid
|
|
||||||
store.update(host, data)
|
|
||||||
.onFailure { exception ->
|
|
||||||
Timber.e(exception, "Failed to parse cached Element .well-known data for $host, deleting cache")
|
|
||||||
store.delete(host)
|
|
||||||
}
|
|
||||||
parsed
|
|
||||||
}
|
|
||||||
.toWellknownRetrieverResult(host)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> Result<T>.toWellknownRetrieverResult(host: String): WellknownRetrieverResult<T> = fold(
|
|
||||||
onSuccess = {
|
|
||||||
WellknownRetrieverResult.Success(it)
|
|
||||||
},
|
|
||||||
onFailure = {
|
|
||||||
Timber.e(it, "Failed to retrieve Element .well-known from $host")
|
|
||||||
// This check on message value is not ideal but this is what we got from the SDK.
|
|
||||||
it.toWellknownRetrieverResult()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun <T> Throwable.toWellknownRetrieverResult(): WellknownRetrieverResult<T> {
|
|
||||||
return if ((this as? ClientException.Generic)?.message?.contains("404") == true) {
|
|
||||||
WellknownRetrieverResult.NotFound
|
|
||||||
} else {
|
|
||||||
WellknownRetrieverResult.Error(this as Exception)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun url(host: String, source: EnterpriseRemoteConfigSource): String {
|
|
||||||
val elementWellKnownUrl = "https://$host/.well-known/element/element.json"
|
|
||||||
return when (source) {
|
|
||||||
EnterpriseRemoteConfigSource.ESS_CONFIG -> enterpriseService.essConfigEndpointUrl(host) ?: elementWellKnownUrl
|
|
||||||
EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT -> elementWellKnownUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-71
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Example:
|
|
||||||
* <pre>
|
|
||||||
* {
|
|
||||||
* "registration_helper_url": "https://element.io",
|
|
||||||
* "enforce_element_pro": true,
|
|
||||||
* "rageshake_url": "https://example.org/rageshake",
|
|
||||||
* "brand_color": "#FF0000",
|
|
||||||
* "notification_sound": "ring.flac",
|
|
||||||
* "idp_app_scheme": "io.element.app",
|
|
||||||
* "custom_recovery_passphrase": {
|
|
||||||
* "min_character_count": 8
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* .
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class InternalElementWellKnown(
|
|
||||||
@SerialName("registration_helper_url")
|
|
||||||
val registrationHelperUrl: String? = null,
|
|
||||||
@SerialName("enforce_element_pro")
|
|
||||||
val enforceElementPro: Boolean? = null,
|
|
||||||
@SerialName("rageshake_url")
|
|
||||||
val rageshakeUrl: String? = null,
|
|
||||||
@SerialName("brand_color")
|
|
||||||
val brandColor: String? = null,
|
|
||||||
@SerialName("notification_sound")
|
|
||||||
val notificationSound: String? = null,
|
|
||||||
@SerialName("idp_app_scheme")
|
|
||||||
val identityProviderAppScheme: String? = null,
|
|
||||||
@SerialName("custom_recovery_passphrase")
|
|
||||||
val customRecoveryPassphrase: InternalCustomRecoveryPassphrase? = null,
|
|
||||||
@SerialName("content_scanner_url")
|
|
||||||
val contentScannerUrl: String? = null,
|
|
||||||
@SerialName("force_disable_e2ee")
|
|
||||||
val forceDisableE2EE: Boolean? = null,
|
|
||||||
@SerialName("map_tiler")
|
|
||||||
val mapTilerConfig: InternalMapTilerConfig? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class InternalCustomRecoveryPassphrase(
|
|
||||||
@SerialName("min_character_count")
|
|
||||||
val minCharacterCount: Int? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class InternalMapTilerConfig(
|
|
||||||
@SerialName("api_key")
|
|
||||||
val apiKey: String,
|
|
||||||
@SerialName("light_style_id")
|
|
||||||
val lightStyleId: String? = null,
|
|
||||||
@SerialName("dark_style_id")
|
|
||||||
val darkStyleId: String? = null,
|
|
||||||
@SerialName("base_url")
|
|
||||||
val baseUrl: String? = null,
|
|
||||||
)
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2023-2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
|
||||||
* <pre>
|
|
||||||
* {
|
|
||||||
* "m.homeserver": {
|
|
||||||
* "base_url": "https://matrix.org"
|
|
||||||
* },
|
|
||||||
* "m.identity_server": {
|
|
||||||
* "base_url": "https://vector.im"
|
|
||||||
* },
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* .
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class InternalWellKnown(
|
|
||||||
@SerialName("m.homeserver")
|
|
||||||
val homeServer: InternalWellKnownBaseConfig? = null,
|
|
||||||
@SerialName("m.identity_server")
|
|
||||||
val identityServer: InternalWellKnownBaseConfig? = null,
|
|
||||||
)
|
|
||||||
-27
@@ -1,27 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2023-2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
/**
|
|
||||||
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
|
||||||
* <pre>
|
|
||||||
* {
|
|
||||||
* "base_url": "https://element.io"
|
|
||||||
* }
|
|
||||||
* </pre>
|
|
||||||
* .
|
|
||||||
*/
|
|
||||||
@Serializable
|
|
||||||
data class InternalWellKnownBaseConfig(
|
|
||||||
@SerialName("base_url")
|
|
||||||
val baseURL: String? = null
|
|
||||||
)
|
|
||||||
-59
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import io.element.android.libraries.core.log.logger.LoggerTag
|
|
||||||
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
private val loggerTag = LoggerTag("Wellknown")
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Floor for the recovery passphrase minimum length. A value of 1 guarantees the derived passphrase is
|
|
||||||
* never empty (an empty passphrase derives a recoverable but effectively unprotected secret-storage
|
|
||||||
* key). Anything stronger than non-empty is left to the homeserver operator's configuration.
|
|
||||||
*/
|
|
||||||
private const val MINIMUM_PASSPHRASE_CHARACTER_COUNT = 1
|
|
||||||
|
|
||||||
fun InternalElementWellKnown.map() = ElementWellKnown(
|
|
||||||
registrationHelperUrl = registrationHelperUrl,
|
|
||||||
enforceElementPro = enforceElementPro,
|
|
||||||
rageshakeUrl = rageshakeUrl,
|
|
||||||
brandColor = brandColor,
|
|
||||||
notificationSound = notificationSound,
|
|
||||||
identityProviderAppScheme = identityProviderAppScheme,
|
|
||||||
customRecoveryPassphrase = customRecoveryPassphrase?.toPublic(),
|
|
||||||
contentScannerUrl = contentScannerUrl,
|
|
||||||
forceDisableE2EE = forceDisableE2EE,
|
|
||||||
mapTilerConfig = mapTilerConfig?.let {
|
|
||||||
MapTilerConfig(
|
|
||||||
apiKey = it.apiKey,
|
|
||||||
lightStyleId = it.lightStyleId,
|
|
||||||
darkStyleId = it.darkStyleId,
|
|
||||||
baseUrl = it.baseUrl,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun InternalCustomRecoveryPassphrase.toPublic(): CustomRecoveryPassphrase {
|
|
||||||
// Whenever the homeserver advertises the settings block we run the custom passphrase flow, flooring
|
|
||||||
// the minimum at 1 so the passphrase can never be empty even if the server omits min_character_count
|
|
||||||
// or advertises a non-positive value. The operator owns any stronger minimum.
|
|
||||||
val min = (minCharacterCount ?: MINIMUM_PASSPHRASE_CHARACTER_COUNT).coerceAtLeast(MINIMUM_PASSPHRASE_CHARACTER_COUNT)
|
|
||||||
if (min != minCharacterCount) {
|
|
||||||
Timber.tag(loggerTag.value).w(
|
|
||||||
"custom_recovery_passphrase.min_character_count was %s; flooring to %d",
|
|
||||||
minCharacterCount,
|
|
||||||
min,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return CustomRecoveryPassphrase(minCharacterCount = min)
|
|
||||||
}
|
|
||||||
-41
@@ -1,41 +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.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import dev.zacsweers.metro.BindingContainer
|
|
||||||
import dev.zacsweers.metro.ContributesTo
|
|
||||||
import dev.zacsweers.metro.Provides
|
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
import io.element.android.libraries.di.annotations.SessionCoroutineScope
|
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellknownStore
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
@BindingContainer
|
|
||||||
@ContributesTo(SessionScope::class)
|
|
||||||
object SessionWellknownModule {
|
|
||||||
@Provides
|
|
||||||
fun provideWellKnownRetriever(
|
|
||||||
elementWellknownStoreFactory: ElementWellknownStore.Factory,
|
|
||||||
enterpriseService: EnterpriseService,
|
|
||||||
elementWellKnownParser: ElementWellKnownParser,
|
|
||||||
urlContentFetcher: UrlContentFetcher,
|
|
||||||
@SessionCoroutineScope coroutineScope: CoroutineScope,
|
|
||||||
): WellknownRetriever {
|
|
||||||
return DefaultWellknownRetriever(
|
|
||||||
elementWellknownStoreFactory = elementWellknownStoreFactory,
|
|
||||||
enterpriseService = enterpriseService,
|
|
||||||
elementWellKnownParser = elementWellKnownParser,
|
|
||||||
urlContentFetcher = urlContentFetcher,
|
|
||||||
coroutineScope = coroutineScope
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2023-2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import retrofit2.http.GET
|
|
||||||
|
|
||||||
internal interface WellknownAPI {
|
|
||||||
@GET(".well-known/matrix/client")
|
|
||||||
suspend fun getWellKnown(): InternalWellKnown
|
|
||||||
|
|
||||||
@GET(".well-known/element/element.json")
|
|
||||||
suspend fun getElementWellKnown(): InternalElementWellKnown
|
|
||||||
}
|
|
||||||
-33
@@ -1,33 +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.libraries.wellknown.api
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class CustomRecoveryPassphraseTest {
|
|
||||||
@Test
|
|
||||||
fun `empty input never satisfies a positive minimum`() {
|
|
||||||
assertThat(CustomRecoveryPassphrase(minCharacterCount = 1).isSatisfiedBy("")).isFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `input shorter than minimum is not satisfied`() {
|
|
||||||
assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abc")).isFalse()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `input exactly at minimum is satisfied`() {
|
|
||||||
assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abcdefgh")).isTrue()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `input longer than minimum is satisfied`() {
|
|
||||||
assertThat(CustomRecoveryPassphrase(minCharacterCount = 4).isSatisfiedBy("abcdefgh")).isTrue()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-174
@@ -1,174 +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.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
|
||||||
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
|
|
||||||
import io.element.android.libraries.cachestore.api.CacheData
|
|
||||||
import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
|
|
||||||
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
|
|
||||||
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
private const val A_DOMAIN = "matrix.example.com"
|
|
||||||
private const val A_CACHE_KEY = "https://$A_DOMAIN/.well-known/element/element.json"
|
|
||||||
|
|
||||||
// 1 day in millis
|
|
||||||
private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L
|
|
||||||
|
|
||||||
class DefaultWellKnownStoreTest {
|
|
||||||
private val jsonProvider = DefaultJsonProvider()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get returns NotFound when cache is empty`() = runTest {
|
|
||||||
val sut = createDefaultWellKnownStore()
|
|
||||||
assertThat(sut.get(A_DOMAIN)).isEqualTo(WellknownRetrieverResult.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get returns Success when cache has fresh data`() = runTest {
|
|
||||||
val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
|
|
||||||
val jsonString = jsonProvider().encodeToString(wellKnown)
|
|
||||||
val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
|
|
||||||
val cacheStore = InMemoryCacheStore(
|
|
||||||
initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, A_FAKE_TIMESTAMP))
|
|
||||||
)
|
|
||||||
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
|
|
||||||
|
|
||||||
val result = sut.get(A_DOMAIN)
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo(WellknownRetrieverResult.Success(wellKnown.map()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get returns Outdated when cached data has expired`() = runTest {
|
|
||||||
val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
|
|
||||||
val jsonString = jsonProvider().encodeToString(wellKnown)
|
|
||||||
// Store data at timestamp 0, but clock is past the validity window
|
|
||||||
val expiredTimestamp = 0L
|
|
||||||
val nowTimestamp = expiredTimestamp + CACHE_VALIDITY_MILLIS + 1
|
|
||||||
val clock = FakeSystemClock(epochMillisResult = nowTimestamp)
|
|
||||||
val cacheStore = InMemoryCacheStore(
|
|
||||||
initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, expiredTimestamp))
|
|
||||||
)
|
|
||||||
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
|
|
||||||
|
|
||||||
val result = sut.get(A_DOMAIN)
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo(WellknownRetrieverResult.Outdated(wellKnown.map()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get returns Error when cached data is invalid JSON`() = runTest {
|
|
||||||
val cacheStore = InMemoryCacheStore(
|
|
||||||
initialData = mapOf(A_CACHE_KEY to CacheData("not valid json at all", A_FAKE_TIMESTAMP))
|
|
||||||
)
|
|
||||||
val sut = createDefaultWellKnownStore(cacheStore = cacheStore)
|
|
||||||
|
|
||||||
val result = sut.get(A_DOMAIN)
|
|
||||||
|
|
||||||
assertThat(result).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `update stores data and get returns Success`() = runTest {
|
|
||||||
val wellKnown = """
|
|
||||||
{
|
|
||||||
"registration_helper_url": "https://element.io",
|
|
||||||
"enforce_element_pro": true,
|
|
||||||
"rageshake_url": "https://example.org/rageshake",
|
|
||||||
"brand_color": "#FF0000",
|
|
||||||
"notification_sound": "ring.flac",
|
|
||||||
"idp_app_scheme": "io.element.app",
|
|
||||||
"custom_recovery_passphrase": {
|
|
||||||
"min_character_count": 8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""".trimIndent()
|
|
||||||
val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
|
|
||||||
val cacheStore = InMemoryCacheStore()
|
|
||||||
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
|
|
||||||
|
|
||||||
val updateResult = sut.update(A_DOMAIN, wellKnown)
|
|
||||||
assertThat(updateResult.isSuccess).isTrue()
|
|
||||||
|
|
||||||
val result = sut.get(A_DOMAIN)
|
|
||||||
assertThat(result).isEqualTo(WellknownRetrieverResult.Success(anElementWellKnown(
|
|
||||||
registrationHelperUrl = "https://element.io",
|
|
||||||
enforceElementPro = true,
|
|
||||||
rageshakeUrl = "https://example.org/rageshake",
|
|
||||||
brandColor = "#FF0000",
|
|
||||||
notificationSound = "ring.flac",
|
|
||||||
identityProviderAppScheme = "io.element.app",
|
|
||||||
customRecoveryPassphrase = CustomRecoveryPassphrase(8),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `delete removes data and get returns NotFound`() = runTest {
|
|
||||||
val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
|
|
||||||
val jsonString = jsonProvider().encodeToString(wellKnown)
|
|
||||||
val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
|
|
||||||
val cacheStore = InMemoryCacheStore(
|
|
||||||
initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, A_FAKE_TIMESTAMP))
|
|
||||||
)
|
|
||||||
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
|
|
||||||
|
|
||||||
val deleteResult = sut.delete(A_DOMAIN)
|
|
||||||
assertThat(deleteResult.isSuccess).isTrue()
|
|
||||||
|
|
||||||
assertThat(sut.get(A_DOMAIN)).isEqualTo(WellknownRetrieverResult.NotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get returns data at the exact cache validity boundary as Success`() = runTest {
|
|
||||||
val wellKnown = InternalElementWellKnown(notificationSound = "ping.flac")
|
|
||||||
val jsonString = jsonProvider().encodeToString(wellKnown)
|
|
||||||
// Exactly at the boundary: epochMillis == updatedAt + CACHE_VALIDITY_MILLIS → not expired
|
|
||||||
val storedAt = 1000L
|
|
||||||
val nowAtBoundary = storedAt + CACHE_VALIDITY_MILLIS
|
|
||||||
val clock = FakeSystemClock(epochMillisResult = nowAtBoundary)
|
|
||||||
val cacheStore = InMemoryCacheStore(
|
|
||||||
initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, storedAt))
|
|
||||||
)
|
|
||||||
val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
|
|
||||||
|
|
||||||
val result = sut.get(A_DOMAIN)
|
|
||||||
|
|
||||||
assertThat(result).isEqualTo(WellknownRetrieverResult.Success(wellKnown.map()))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `prefix is used`() = runTest {
|
|
||||||
val prefix = "test-prefix"
|
|
||||||
val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
|
|
||||||
val cacheStore = InMemoryCacheStore(
|
|
||||||
initialData = mapOf("$prefix-$A_DOMAIN" to CacheData("{}", A_FAKE_TIMESTAMP))
|
|
||||||
)
|
|
||||||
val sut = createDefaultWellKnownStore(prefix = prefix, cacheStore = cacheStore, systemClock = clock)
|
|
||||||
|
|
||||||
assertThat(sut.get(A_DOMAIN)).isInstanceOf(WellknownRetrieverResult.Success::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createDefaultWellKnownStore(
|
|
||||||
prefix: String? = null,
|
|
||||||
cacheStore: InMemoryCacheStore = InMemoryCacheStore(),
|
|
||||||
systemClock: FakeSystemClock = FakeSystemClock(),
|
|
||||||
elementWellKnownParser: ElementWellKnownParser = DefaultElementWellKnownParser(jsonProvider),
|
|
||||||
) = DefaultWellKnownStore(
|
|
||||||
prefix = prefix,
|
|
||||||
cacheStore = cacheStore,
|
|
||||||
systemClock = systemClock,
|
|
||||||
elementWellKnownParser = elementWellKnownParser,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-241
@@ -1,241 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@file:OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
|
|
||||||
package io.element.android.libraries.wellknown.impl
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import io.element.android.features.enterprise.test.FakeEnterpriseService
|
|
||||||
import io.element.android.features.wellknown.test.FakeElementWellknownStore
|
|
||||||
import io.element.android.features.wellknown.test.aMapTilerConfig
|
|
||||||
import io.element.android.features.wellknown.test.anElementWellKnown
|
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
|
||||||
import io.element.android.libraries.matrix.api.exception.ClientException
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnownParser
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellknownStore
|
|
||||||
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
|
||||||
import io.element.android.tests.testutils.lambda.value
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
|
||||||
import kotlinx.coroutines.test.runCurrent
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Test
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
class DefaultWellknownRetrieverTest {
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown calls resolver and parser`() = runTest {
|
|
||||||
val parser = lambdaRecorder<String, Result<ElementWellKnown>> { Result.success(anElementWellKnown()) }
|
|
||||||
val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
|
|
||||||
val sut = createDefaultWellknownRetriever(
|
|
||||||
elementWellKnownParser = parser,
|
|
||||||
urlResolver = resolver,
|
|
||||||
)
|
|
||||||
|
|
||||||
sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)
|
|
||||||
|
|
||||||
resolver.assertions().isCalledOnce().with(value(WELLKNOWN_URL))
|
|
||||||
parser.assertions().isCalledOnce().with(value(WELLKNOWN_CONTENT))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown parser error`() = runTest {
|
|
||||||
val sut = createDefaultWellknownRetriever(
|
|
||||||
elementWellKnownParser = { Result.failure(IllegalStateException("Invalid JSON")) },
|
|
||||||
)
|
|
||||||
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
|
|
||||||
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown network error`() = runTest {
|
|
||||||
val resolver = SpyUrlContentFetcher { Result.failure(IllegalStateException("Network error")) }
|
|
||||||
val sut = createDefaultWellknownRetriever(urlResolver = resolver)
|
|
||||||
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
|
|
||||||
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown 404 http error counts as not found`() = runTest {
|
|
||||||
val resolver = SpyUrlContentFetcher { Result.failure(ClientException.Generic("Http error: 404", null)) }
|
|
||||||
val sut = createDefaultWellknownRetriever(urlResolver = resolver)
|
|
||||||
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
|
|
||||||
.isInstanceOf(WellknownRetrieverResult.NotFound::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown will return an error if the provided host is not valid`() = runTest {
|
|
||||||
val sut = createDefaultWellknownRetriever()
|
|
||||||
assertThat(sut.getElementWellKnown("!not valid", EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
|
|
||||||
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown will return a cached value if present instead of fetching`() = runTest {
|
|
||||||
val host = URL(WELLKNOWN_URL).host.ensureProtocol()
|
|
||||||
val sut = createDefaultWellknownRetriever(
|
|
||||||
cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Success(expectedElementWellKnown))) }
|
|
||||||
)
|
|
||||||
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
|
|
||||||
.isInstanceOf(WellknownRetrieverResult.Success::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown will return an outdated value first then fetch the current one`() = runTest {
|
|
||||||
val host = URL(WELLKNOWN_URL).host.ensureProtocol()
|
|
||||||
val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
|
|
||||||
val sut = createDefaultWellknownRetriever(
|
|
||||||
urlResolver = resolver,
|
|
||||||
cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Outdated(expectedElementWellKnown))) }
|
|
||||||
)
|
|
||||||
// The first call returns the outdated value
|
|
||||||
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
|
|
||||||
.isInstanceOf(WellknownRetrieverResult.Outdated::class.java)
|
|
||||||
// The resolver is not called yet because the fetch is done in the background
|
|
||||||
resolver.assertions().isNeverCalled()
|
|
||||||
|
|
||||||
// We give it some time to run the background fetch
|
|
||||||
runCurrent()
|
|
||||||
|
|
||||||
// The resolver is called during the background fetch
|
|
||||||
resolver.assertions().isCalledOnce().with(value(WELLKNOWN_URL))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown will return an error if it happens when trying to retrieve the cached value`() = runTest {
|
|
||||||
val host = URL(WELLKNOWN_URL).host.ensureProtocol()
|
|
||||||
val sut = createDefaultWellknownRetriever(
|
|
||||||
cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Error(IllegalStateException("BOOM")))) }
|
|
||||||
)
|
|
||||||
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
|
|
||||||
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown will return an error if it fails to serialize the retrieved value into the cache`() = runTest {
|
|
||||||
val sut = createDefaultWellknownRetriever(
|
|
||||||
elementWellKnownParser = { Result.failure(IllegalStateException("BOOM")) },
|
|
||||||
)
|
|
||||||
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
|
|
||||||
.isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WELLKNOWN_ENDPOINT source creates a store with no prefix`() = runTest {
|
|
||||||
val storeFactory = lambdaRecorder<String?, ElementWellknownStore> { FakeElementWellknownStore() }
|
|
||||||
val sut = createDefaultWellknownRetriever(cacheStoreFactory = storeFactory)
|
|
||||||
|
|
||||||
sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)
|
|
||||||
|
|
||||||
storeFactory.assertions().isCalledOnce().with(value(null))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `ESS_CONFIG source creates a store with ess prefix and checks the ESS config endpoint`() = runTest {
|
|
||||||
val essConfigUrl = "https://ess-config.example.com"
|
|
||||||
val storeFactory = lambdaRecorder<String?, ElementWellknownStore> { FakeElementWellknownStore() }
|
|
||||||
val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
|
|
||||||
val sut = createDefaultWellknownRetriever(
|
|
||||||
cacheStoreFactory = storeFactory,
|
|
||||||
enterpriseService = FakeEnterpriseService(
|
|
||||||
overrideWellKnownResult = { null },
|
|
||||||
essConfigEndpointUrlResult = { essConfigUrl },
|
|
||||||
),
|
|
||||||
urlResolver = resolver,
|
|
||||||
)
|
|
||||||
|
|
||||||
sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.ESS_CONFIG)
|
|
||||||
|
|
||||||
storeFactory.assertions().isCalledOnce().with(value("ess_config"))
|
|
||||||
resolver.assertions().isCalledOnce().with(value(essConfigUrl))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `get element wellknown was overridden`() = runTest {
|
|
||||||
val wellKnown = anElementWellKnown()
|
|
||||||
|
|
||||||
val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
|
|
||||||
|
|
||||||
val sut = createDefaultWellknownRetriever(
|
|
||||||
enterpriseService = FakeEnterpriseService(
|
|
||||||
overrideWellKnownResult = { wellKnown },
|
|
||||||
),
|
|
||||||
urlResolver = resolver,
|
|
||||||
)
|
|
||||||
|
|
||||||
// The overridden value is returned
|
|
||||||
assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)).isEqualTo(
|
|
||||||
WellknownRetrieverResult.Success(wellKnown)
|
|
||||||
)
|
|
||||||
|
|
||||||
// And the endpoint is never hit
|
|
||||||
resolver.assertions().isNeverCalled()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TestScope.createDefaultWellknownRetriever(
|
|
||||||
cacheStoreFactory: ElementWellknownStore.Factory = { FakeElementWellknownStore() },
|
|
||||||
elementWellKnownParser: ElementWellKnownParser = { Result.success(expectedElementWellKnown) },
|
|
||||||
enterpriseService: FakeEnterpriseService = FakeEnterpriseService(overrideWellKnownResult = { null }),
|
|
||||||
urlResolver: UrlContentFetcher = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) },
|
|
||||||
) = DefaultWellknownRetriever(
|
|
||||||
elementWellknownStoreFactory = cacheStoreFactory,
|
|
||||||
enterpriseService = enterpriseService,
|
|
||||||
elementWellKnownParser = elementWellKnownParser,
|
|
||||||
urlContentFetcher = urlResolver,
|
|
||||||
coroutineScope = backgroundScope,
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val WELLKNOWN_URL = "https://user.domain.org/.well-known/element/element.json"
|
|
||||||
private const val WELLKNOWN_CONTENT = """{
|
|
||||||
"registration_helper_url": "a_registration_url",
|
|
||||||
"enforce_element_pro": true,
|
|
||||||
"rageshake_url": "a_rageshake_url",
|
|
||||||
"brand_color": "#FF0000",
|
|
||||||
"notification_sound": "a_notification_sound.flac",
|
|
||||||
"idp_app_scheme": "an_app_scheme",
|
|
||||||
"content_scanner_url": "https://content-scanner.example.com",
|
|
||||||
"force_disable_e2ee": false,
|
|
||||||
"map_tiler": {
|
|
||||||
"api_key": "test_api_key",
|
|
||||||
"base_url": "https://tile-server.example.com"
|
|
||||||
}
|
|
||||||
}"""
|
|
||||||
|
|
||||||
private val expectedElementWellKnown = ElementWellKnown(
|
|
||||||
registrationHelperUrl = "a_registration_url",
|
|
||||||
enforceElementPro = true,
|
|
||||||
rageshakeUrl = "a_rageshake_url",
|
|
||||||
brandColor = "#FF0000",
|
|
||||||
notificationSound = "a_notification_sound.flac",
|
|
||||||
identityProviderAppScheme = "an_app_scheme",
|
|
||||||
contentScannerUrl = "https://content-scanner.example.com",
|
|
||||||
customRecoveryPassphrase = null,
|
|
||||||
forceDisableE2EE = false,
|
|
||||||
mapTilerConfig = aMapTilerConfig(
|
|
||||||
apiKey = "test_api_key",
|
|
||||||
baseUrl = "https://tile-server.example.com",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class SpyUrlContentFetcher(lambda: (String) -> Result<ByteArray>) : UrlContentFetcher {
|
|
||||||
private val spy = lambdaRecorder<String, Result<ByteArray>> { lambda(it) }
|
|
||||||
|
|
||||||
override suspend fun getUrl(url: String): Result<ByteArray> {
|
|
||||||
return spy(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun assertions() = spy.assertions()
|
|
||||||
}
|
|
||||||
-53
@@ -1,53 +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.wellknown.test
|
|
||||||
|
|
||||||
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
|
|
||||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellknownStore
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
import io.element.android.libraries.wellknown.impl.InternalElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.impl.map
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.json.JsonDecodingException
|
|
||||||
import java.net.URL
|
|
||||||
|
|
||||||
class FakeElementWellknownStore(
|
|
||||||
initialData: Map<String, WellknownRetrieverResult<ElementWellKnown>> = emptyMap(),
|
|
||||||
private val jsonProvider: JsonProvider = DefaultJsonProvider(),
|
|
||||||
) : ElementWellknownStore {
|
|
||||||
private val cachedData = mutableMapOf<String, WellknownRetrieverResult<ElementWellKnown>>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
initialData.forEach { (url, result) ->
|
|
||||||
cachedData[URL(url).host] = result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun get(domain: String): WellknownRetrieverResult<ElementWellKnown> {
|
|
||||||
return cachedData[domain] ?: WellknownRetrieverResult.NotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalSerializationApi::class)
|
|
||||||
override suspend fun update(domain: String, wellknown: String): Result<Unit> {
|
|
||||||
val json = jsonProvider()
|
|
||||||
val mapped = try {
|
|
||||||
json.decodeFromString<InternalElementWellKnown>(wellknown).map()
|
|
||||||
} catch (e: JsonDecodingException) {
|
|
||||||
return Result.failure(e)
|
|
||||||
}
|
|
||||||
cachedData[domain] = WellknownRetrieverResult.Success(mapped)
|
|
||||||
return Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun delete(domain: String): Result<Unit> {
|
|
||||||
cachedData.remove(domain)
|
|
||||||
return Result.success(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.wellknown.test
|
|
||||||
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
|
||||||
|
|
||||||
class FakeWellknownRetriever(
|
|
||||||
private val getElementWellKnownResult: (String, EnterpriseRemoteConfigSource) -> WellknownRetrieverResult<ElementWellKnown> = { _, _ ->
|
|
||||||
WellknownRetrieverResult.NotFound
|
|
||||||
},
|
|
||||||
) : WellknownRetriever {
|
|
||||||
override suspend fun getElementWellKnown(host: String, source: EnterpriseRemoteConfigSource): WellknownRetrieverResult<ElementWellKnown> {
|
|
||||||
return getElementWellKnownResult(host, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-17
@@ -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.wellknown.test
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.UrlContentFetcher
|
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
|
||||||
|
|
||||||
class FakeWellknownRetrieverFactory(
|
|
||||||
private val wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
|
|
||||||
) : WellknownRetriever.Factory {
|
|
||||||
override fun create(urlContentFetcher: UrlContentFetcher): WellknownRetriever = wellknownRetriever
|
|
||||||
}
|
|
||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Element Creations Ltd.
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.wellknown.test
|
|
||||||
|
|
||||||
import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
|
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
|
||||||
import io.element.android.libraries.wellknown.api.MapTilerConfig
|
|
||||||
|
|
||||||
fun anElementWellKnown(
|
|
||||||
registrationHelperUrl: String? = null,
|
|
||||||
enforceElementPro: Boolean? = null,
|
|
||||||
rageshakeUrl: String? = null,
|
|
||||||
brandColor: String? = null,
|
|
||||||
notificationSound: String? = null,
|
|
||||||
identityProviderAppScheme: String? = null,
|
|
||||||
customRecoveryPassphrase: CustomRecoveryPassphrase? = null,
|
|
||||||
contentScannerUrl: String? = null,
|
|
||||||
forceDisableE2EE: Boolean? = null,
|
|
||||||
mapTilerConfig: MapTilerConfig? = null,
|
|
||||||
) = ElementWellKnown(
|
|
||||||
registrationHelperUrl = registrationHelperUrl,
|
|
||||||
enforceElementPro = enforceElementPro,
|
|
||||||
rageshakeUrl = rageshakeUrl,
|
|
||||||
brandColor = brandColor,
|
|
||||||
notificationSound = notificationSound,
|
|
||||||
identityProviderAppScheme = identityProviderAppScheme,
|
|
||||||
customRecoveryPassphrase = customRecoveryPassphrase,
|
|
||||||
contentScannerUrl = contentScannerUrl,
|
|
||||||
forceDisableE2EE = forceDisableE2EE,
|
|
||||||
mapTilerConfig = mapTilerConfig,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun aCustomRecoveryPassphrase(
|
|
||||||
minCharacterCount: Int = 8,
|
|
||||||
) = CustomRecoveryPassphrase(
|
|
||||||
minCharacterCount = minCharacterCount,
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
+1
-3
@@ -9,7 +9,6 @@
|
|||||||
package io.element.android.services.analyticsproviders.posthog
|
package io.element.android.services.analyticsproviders.posthog
|
||||||
|
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.enterprise.api.EnterpriseService
|
|
||||||
import io.element.android.libraries.core.extensions.isElement
|
import io.element.android.libraries.core.extensions.isElement
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
import io.element.android.libraries.core.meta.BuildType
|
import io.element.android.libraries.core.meta.BuildType
|
||||||
@@ -17,10 +16,9 @@ import io.element.android.libraries.core.meta.BuildType
|
|||||||
@Inject
|
@Inject
|
||||||
class PosthogEndpointConfigProvider(
|
class PosthogEndpointConfigProvider(
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val enterpriseService: EnterpriseService,
|
|
||||||
) {
|
) {
|
||||||
fun provide(): PosthogEndpointConfig? {
|
fun provide(): PosthogEndpointConfig? {
|
||||||
return if (enterpriseService.isEnterpriseBuild) {
|
return if (buildMeta.isEnterpriseBuild) {
|
||||||
PosthogEndpointConfig(
|
PosthogEndpointConfig(
|
||||||
host = BuildConfig.POSTHOG_HOST,
|
host = BuildConfig.POSTHOG_HOST,
|
||||||
apiKey = BuildConfig.POSTHOG_APIKEY,
|
apiKey = BuildConfig.POSTHOG_APIKEY,
|
||||||
|
|||||||
Reference in New Issue
Block a user