diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 7b86b434b9..054e9525c7 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.uiCommon) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.wellknown.api) implementation(projects.features.login.api) implementation(libs.coil) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 14230d7c5a..d7b768132b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -11,8 +11,10 @@ package io.element.android.appnav import android.os.Parcelable import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope @@ -49,11 +51,13 @@ import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.SessionEnterpriseService +import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider 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.FtueState import io.element.android.features.home.api.HomeEntryPoint import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint +import io.element.android.features.location.api.LocalMapTilerConfig import io.element.android.features.location.api.live.ActiveLiveLocationShareManager import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus @@ -153,6 +157,7 @@ class LoggedInFlowNode( private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher, private val createRoomEntryPoint: CreateRoomEntryPoint, private val activeLiveLocationShareManager: ActiveLiveLocationShareManager, + private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider, ) : BaseFlowNode( backstack = BackStack( initialElement = NavTarget.Placeholder, @@ -677,6 +682,12 @@ class LoggedInFlowNode( val colors by remember { enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId) }.collectAsState(SemanticColorsLightDark.default) + + val currentMapTilerConfig = LocalMapTilerConfig.current + val updatedMapTilerConfig by produceState(currentMapTilerConfig) { + value = remoteEnterpriseConfigProvider.get(matrixClient.sessionId).dataOrNull()?.mapTilerConfig ?: currentMapTilerConfig + } + ElementThemeApp( appPreferencesStore = appPreferencesStore, featureFlagService = featureFlagService, @@ -684,16 +695,18 @@ class LoggedInFlowNode( compoundDark = colors.dark, buildMeta = buildMeta, ) { - val isOnline by syncService.isOnline.collectAsState() - ConnectivityIndicatorContainer( - isOnline = isOnline, - modifier = modifier, - ) { contentModifier -> - Box(modifier = contentModifier) { - val ftueState by ftueService.state.collectAsState() - BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack)) - if (ftueState is FtueState.Complete) { - PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) + CompositionLocalProvider(LocalMapTilerConfig provides updatedMapTilerConfig) { + val isOnline by syncService.isOnline.collectAsState() + ConnectivityIndicatorContainer( + isOnline = isOnline, + modifier = modifier, + ) { contentModifier -> + Box(modifier = contentModifier) { + val ftueState by ftueService.state.collectAsState() + BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack)) + if (ftueState is FtueState.Complete) { + PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) + } } } } diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts index f8377389f1..b3eeacf87f 100644 --- a/features/location/api/build.gradle.kts +++ b/features/location/api/build.gradle.kts @@ -70,6 +70,7 @@ dependencies { implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) + implementation(projects.libraries.wellknown.api) implementation(libs.coil.compose) implementation(libs.datetime) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocalMapTilerConfig.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocalMapTilerConfig.kt new file mode 100644 index 0000000000..a2212119f5 --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocalMapTilerConfig.kt @@ -0,0 +1,23 @@ +/* + * 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.location.api + +import androidx.compose.runtime.staticCompositionLocalOf +import io.element.android.libraries.wellknown.api.MapTilerConfig + +/** + * Provides a [MapTilerConfig] for the MapLibre maps. + */ +val LocalMapTilerConfig = staticCompositionLocalOf { + MapTilerConfig( + apiKey = BuildConfig.MAPTILER_API_KEY, + lightStyleId = BuildConfig.MAPTILER_LIGHT_MAP_ID, + darkStyleId = BuildConfig.MAPTILER_DARK_MAP_ID, + baseUrl = BuildConfig.MAPTILER_BASE_URL + ) +} diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt index c16b1ae560..33e454722f 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt @@ -38,13 +38,14 @@ import coil3.request.ImageRequest import coil3.request.SuccessResult import io.element.android.compound.theme.ElementTheme import io.element.android.features.location.api.internal.StaticMapPlaceholder -import io.element.android.features.location.api.internal.StaticMapUrlBuilder import io.element.android.features.location.api.internal.centerBottomEdge +import io.element.android.features.location.api.internal.rememberStaticMapBuilder import io.element.android.libraries.designsystem.components.LocationPin import io.element.android.libraries.designsystem.components.PinVariant import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight 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. @@ -57,6 +58,7 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables */ @Composable fun StaticMapView( + mapTilerConfig: MapTilerConfig?, location: Location?, zoom: Double, pinVariant: PinVariant, @@ -94,6 +96,7 @@ fun StaticMapView( } // Cases 3 & 4: Non-null location - fetch map else -> LoadableMapContent( + mapTilerConfig = mapTilerConfig, location = location, zoom = zoom, pinVariant = pinVariant, @@ -124,6 +127,7 @@ private fun BoxWithConstraintsScope.StaleMapContent( @Composable private fun BoxWithConstraintsScope.LoadableMapContent( + mapTilerConfig: MapTilerConfig?, location: Location, zoom: Double, pinVariant: PinVariant, @@ -132,7 +136,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent( ) { val context = LocalContext.current var retryHash by remember { mutableIntStateOf(0) } - val builder = remember { StaticMapUrlBuilder() } + val builder = rememberStaticMapBuilder(mapTilerConfig) val (painter, state, contentScale) = if (LocalInspectionMode.current) { val painter = painterResource(CommonDrawables.sample_map) @@ -203,6 +207,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent( @Composable internal fun StaticMapViewPreview() = ElementPreview { StaticMapView( + mapTilerConfig = null, location = Location(0.0, 0.0), zoom = 0.0, contentDescription = null, diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt index 666ca07a08..78eae2f633 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilder.kt @@ -9,6 +9,7 @@ package io.element.android.features.location.api.internal import io.element.android.features.location.api.BuildConfig +import io.element.android.libraries.wellknown.api.MapTilerConfig import kotlin.math.roundToInt /** @@ -29,6 +30,13 @@ internal class MapTilerStaticMapUrlBuilder( darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID, ) + constructor(mapTilerConfig: MapTilerConfig) : this( + baseUrl = (mapTilerConfig.baseUrl ?: BuildConfig.MAPTILER_BASE_URL).removeSuffix("/"), + apiKey = mapTilerConfig.apiKey.takeIf { it.isNotBlank() } ?: BuildConfig.MAPTILER_API_KEY, + lightMapId = mapTilerConfig.lightStyleId ?: BuildConfig.MAPTILER_LIGHT_MAP_ID, + darkMapId = mapTilerConfig.darkStyleId ?: BuildConfig.MAPTILER_DARK_MAP_ID, + ) + override fun build( lat: Double, lon: Double, diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt index 0ff18a3767..10aef90116 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilder.kt @@ -11,6 +11,7 @@ package io.element.android.features.location.api.internal import io.element.android.features.location.api.BuildConfig +import io.element.android.libraries.wellknown.api.MapTilerConfig internal class MapTilerTileServerStyleUriBuilder( private val baseUrl: String, @@ -26,14 +27,17 @@ internal class MapTilerTileServerStyleUriBuilder( ) override fun build( - customMapStyleUrl: String?, + customMapTilerConfig: MapTilerConfig?, darkMode: Boolean, ): String { - return if (customMapStyleUrl.isNullOrBlank()) { - val mapId = if (darkMode) darkMapId else lightMapId - "$baseUrl/$mapId/style.json?key=$apiKey" + val baseUrl = customMapTilerConfig?.baseUrl.takeIf { !it.isNullOrBlank() } ?: baseUrl + val apiKey = customMapTilerConfig?.apiKey ?: apiKey + val mapId = if (darkMode) { + customMapTilerConfig?.darkStyleId ?: darkMapId } else { - customMapStyleUrl + customMapTilerConfig?.lightStyleId ?: lightMapId } + + return "$baseUrl/$mapId/style.json?key=$apiKey" } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt index 6636976828..9bff0ee754 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapUrlBuilder.kt @@ -8,6 +8,10 @@ package io.element.android.features.location.api.internal +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import io.element.android.libraries.wellknown.api.MapTilerConfig + /** * Builds an URL for a 3rd party service provider static maps API. */ @@ -25,4 +29,13 @@ interface StaticMapUrlBuilder { fun isServiceAvailable(): Boolean } -fun StaticMapUrlBuilder(): StaticMapUrlBuilder = MapTilerStaticMapUrlBuilder() +@Composable +fun rememberStaticMapBuilder(mapTilerConfig: MapTilerConfig?): StaticMapUrlBuilder { + return remember(mapTilerConfig) { + if (mapTilerConfig != null) { + MapTilerStaticMapUrlBuilder(mapTilerConfig) + } else { + MapTilerStaticMapUrlBuilder() + } + } +} diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt index 762d580f8e..f0c946f3d0 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/TileServerStyleUriBuilder.kt @@ -11,6 +11,7 @@ package io.element.android.features.location.api.internal import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import io.element.android.compound.theme.ElementTheme +import io.element.android.libraries.wellknown.api.MapTilerConfig /** * Builds a style URI for a MapLibre compatible tile server. @@ -19,7 +20,7 @@ import io.element.android.compound.theme.ElementTheme */ interface TileServerStyleUriBuilder { fun build( - customMapStyleUrl: String?, + customMapTilerConfig: MapTilerConfig?, darkMode: Boolean, ): String } @@ -31,12 +32,12 @@ interface TileServerStyleUriBuilder { */ @Composable fun rememberTileStyleUrl( - customMapStyleUrl: String?, + customMapTileConfig: MapTilerConfig?, ): String { val darkMode = !ElementTheme.isLightTheme - return remember(darkMode, customMapStyleUrl) { + return remember(darkMode, customMapTileConfig) { MapTilerTileServerStyleUriBuilder().build( - customMapStyleUrl = customMapStyleUrl, + customMapTilerConfig = customMapTileConfig, darkMode = darkMode, ) } diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt index addc71c3b9..09aae4f310 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerTileServerStyleUriBuilderTest.kt @@ -9,6 +9,7 @@ package io.element.android.features.location.api.internal import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.wellknown.api.MapTilerConfig import org.junit.Test class MapTilerTileServerStyleUriBuilderTest { @@ -23,7 +24,7 @@ class MapTilerTileServerStyleUriBuilderTest { fun `light map uri`() { assertThat( builder.build( - customMapStyleUrl = null, + customMapTilerConfig = null, darkMode = false, ) ).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey") @@ -33,7 +34,7 @@ class MapTilerTileServerStyleUriBuilderTest { fun `dark map uri`() { assertThat( builder.build( - customMapStyleUrl = null, + customMapTilerConfig = null, darkMode = true, ) ).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey") @@ -43,49 +44,44 @@ class MapTilerTileServerStyleUriBuilderTest { fun `custom map uri light`() { assertThat( builder.build( - customMapStyleUrl = "https://custom.url/style.json", + customMapTilerConfig = MapTilerConfig( + apiKey = "API_KEY", + lightStyleId = "light", + darkStyleId = "dark", + baseUrl = "https://custom.url/style.json" + ), darkMode = false, ) - ).isEqualTo("https://custom.url/style.json") + ).isEqualTo("https://custom.url/style.json/light/style.json?key=API_KEY") } @Test fun `custom map uri dark`() { assertThat( builder.build( - customMapStyleUrl = "https://custom.url/style.json", + customMapTilerConfig = MapTilerConfig( + apiKey = "API_KEY", + lightStyleId = "light", + darkStyleId = "dark", + baseUrl = "https://custom.url/style.json" + ), darkMode = true, ) - ).isEqualTo("https://custom.url/style.json") - } - - @Test - fun `custom map uri with its own api key is not altered`() { - assertThat( - builder.build( - customMapStyleUrl = "https://api.maptiler.com/maps/streets-v2/style.json?key=anOperatorKey", - darkMode = false, - ) - ).isEqualTo("https://api.maptiler.com/maps/streets-v2/style.json?key=anOperatorKey") - } - - @Test - fun `custom map uri with an existing query is not altered`() { - assertThat( - builder.build( - customMapStyleUrl = "https://self.hosted/style.json?foo=bar", - darkMode = true, - ) - ).isEqualTo("https://self.hosted/style.json?foo=bar") + ).isEqualTo("https://custom.url/style.json/dark/style.json?key=API_KEY") } @Test fun `blank custom map uri falls back to the built-in style`() { assertThat( builder.build( - customMapStyleUrl = " ", + customMapTilerConfig = MapTilerConfig( + apiKey = "API_KEY", + lightStyleId = "light", + darkStyleId = "dark", + baseUrl = " " + ), darkMode = false, ) - ).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey") + ).isEqualTo("https://base.url/light/style.json?key=API_KEY") } } diff --git a/features/location/impl/build.gradle.kts b/features/location/impl/build.gradle.kts index 165c32b7c5..43e4f58cab 100644 --- a/features/location/impl/build.gradle.kts +++ b/features/location/impl/build.gradle.kts @@ -26,6 +26,7 @@ setupDependencyInjection() dependencies { api(projects.features.location.api) + implementation(projects.features.enterprise.api) implementation(projects.features.messages.api) implementation(libs.maplibre.compose) implementation(libs.coil) @@ -45,6 +46,7 @@ dependencies { implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) implementation(projects.libraries.sessionStorage.api) + implementation(projects.libraries.wellknown.api) implementation(libs.androidx.datastore.preferences) implementation(libs.datetime) @@ -58,5 +60,7 @@ dependencies { testImplementation(projects.libraries.featureflag.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.sessionStorage.test) + testImplementation(projects.libraries.wellknown.test) testImplementation(projects.features.location.test) + testImplementation(projects.features.enterprise.test) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt index 0eff1b6028..eaa238bed0 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/MapBottomSheetScaffold.kt @@ -50,6 +50,7 @@ import io.element.android.libraries.core.data.tryOrNull 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.CircularProgressIndicator +import io.element.android.libraries.wellknown.api.MapTilerConfig import org.maplibre.compose.camera.CameraState import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.map.MapOptions @@ -66,7 +67,7 @@ import kotlin.math.roundToInt * - Updating camera position padding based on sheet height * - Rendering the MaplibreMap with proper ornament positioning * - * @param customMapStyleUrl Optional custom style URL for the map + * @param customMapTilerConfig Optional custom style URL for the map * @param modifier Modifier for the root layout * @param scaffoldState State for the bottom sheet scaffold * @param cameraState The camera state for the map @@ -83,7 +84,7 @@ import kotlin.math.roundToInt @OptIn(ExperimentalMaterial3Api::class) @Composable fun MapBottomSheetScaffold( - customMapStyleUrl: AsyncData, + customMapTilerConfig: AsyncData, modifier: Modifier = Modifier, scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState( bottomSheetState = rememberBottomSheetState( @@ -137,11 +138,11 @@ fun MapBottomSheetScaffold( val ornamentOptions = mapOptions.ornamentOptions.copy(padding = sheetPadding) val mapOptions = mapOptions.copy(ornamentOptions = ornamentOptions) Box { - when (customMapStyleUrl) { + when (customMapTilerConfig) { is AsyncData.Success -> { MaplibreMap( options = mapOptions, - baseStyle = BaseStyle.Uri(rememberTileStyleUrl(customMapStyleUrl.data)), + baseStyle = BaseStyle.Uri(rememberTileStyleUrl(customMapTilerConfig.data)), modifier = Modifier.fillMaxSize(), cameraState = cameraState, content = mapContent, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt index a3f3cac432..d388e0c5d2 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt @@ -22,6 +22,7 @@ import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import im.vector.app.features.analytics.plan.Composer +import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider 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.MapDefaults @@ -72,6 +73,7 @@ class ShareLocationPresenter( private val liveLocationShareManager: ActiveLiveLocationShareManager, private val liveLocationStore: LiveLocationStore, private val userLocationStateFactory: UserLocationState.Factory, + private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider, ) : Presenter { @AssistedFactory fun interface Factory { @@ -92,9 +94,9 @@ class ShareLocationPresenter( var pendingLiveLocationShare by remember { mutableStateOf(false) } val startLiveLocationAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val currentUser by client.userProfile.collectAsState() - val customMapStyleUrl by produceState(AsyncData.Loading()) { + val customMapConfig by produceState(AsyncData.Loading()) { // Ignore errors - value = AsyncData.Success(client.getMapStyleUrl().getOrNull()) + value = AsyncData.Success(remoteEnterpriseConfigProvider.get(client.sessionId).dataOrNull()?.mapTilerConfig) } val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms -> perms.sendLiveLocationPermissions() @@ -199,7 +201,7 @@ class ShareLocationPresenter( } return ShareLocationState( - customMapStyleUrl = customMapStyleUrl, + customMapTilerConfig = customMapConfig, currentUser = currentUser, dialogState = dialogState, trackUserLocation = trackUserPosition, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt index fb1b803435..d2cce94ef8 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt @@ -13,10 +13,11 @@ import io.element.android.features.location.impl.common.userlocation.UserLocatio import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.wellknown.api.MapTilerConfig import kotlinx.collections.immutable.ImmutableList data class ShareLocationState( - val customMapStyleUrl: AsyncData, + val customMapTilerConfig: AsyncData, val currentUser: MatrixUser, val dialogState: Dialog, val trackUserLocation: Boolean, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt index ed8d46afd3..6d7ff5e612 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.wellknown.api.MapTilerConfig import kotlinx.collections.immutable.persistentListOf import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes @@ -70,13 +71,13 @@ class ShareLocationStateProvider : PreviewParameterProvider canShareLiveLocation = true, ), aShareLocationState( - customMapStyleUrl = AsyncData.Loading(), + customMapTilerConfig = AsyncData.Loading(), ), ) } fun aShareLocationState( - customMapStyleUrl: AsyncData = AsyncData.Success(null), + customMapTilerConfig: AsyncData = AsyncData.Success(null), currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")), dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None, trackUserPosition: Boolean = false, @@ -87,7 +88,7 @@ fun aShareLocationState( eventSink: (ShareLocationEvent) -> Unit = {}, ): ShareLocationState { return ShareLocationState( - customMapStyleUrl = customMapStyleUrl, + customMapTilerConfig = customMapTilerConfig, currentUser = currentUser, dialogState = dialogState, trackUserLocation = trackUserPosition, @@ -95,6 +96,6 @@ fun aShareLocationState( canShareLiveLocation = canShareLiveLocation, appName = appName, startLiveLocationAction = startLiveLocationAction, - eventSink = eventSink + eventSink = eventSink, ) } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt index 8b7106e939..53e7874936 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt @@ -118,7 +118,7 @@ fun ShareLocationView( } MapBottomSheetScaffold( - customMapStyleUrl = state.customMapStyleUrl, + customMapTilerConfig = state.customMapTilerConfig, cameraState = cameraState, modifier = modifier, scaffoldState = scaffoldState, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt index f187e91c53..2d9a4900dd 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject +import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider import io.element.android.features.location.api.Location import io.element.android.features.location.api.ShowLocationMode import io.element.android.features.location.api.live.ActiveLiveLocationShareManager @@ -46,7 +47,7 @@ import io.element.android.libraries.dateformatter.api.DateFormatter 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.AvatarSize -import io.element.android.libraries.matrix.api.MatrixClient +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.getBestName import io.element.android.libraries.matrix.api.room.joinedRoomMembers @@ -64,10 +65,11 @@ import kotlinx.coroutines.launch private val buildMeta: BuildMeta, private val dateFormatter: DateFormatter, private val stringProvider: StringProvider, - private val client: MatrixClient, + private val sessionId: SessionId, private val joinedRoom: JoinedRoom, private val liveLocationShareManager: ActiveLiveLocationShareManager, private val userLocationStateFactory: UserLocationState.Factory, + private val remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider, ) : Presenter { @AssistedFactory fun interface Factory { fun create(mode: ShowLocationMode): ShowLocationPresenter @@ -85,9 +87,9 @@ import kotlinx.coroutines.launch mutableStateOf(LocationConstraintsDialogState.None) } - val customMapStyleUrl by produceState(AsyncData.Loading()) { + val customMapTilerConfig by produceState(AsyncData.Loading()) { // Ignore errors - value = AsyncData.Success(client.getMapStyleUrl().getOrNull()) + value = AsyncData.Success(remoteEnterpriseConfigProvider.get(sessionId).dataOrNull()?.mapTilerConfig) } fun checkLocationConstraints() { @@ -220,7 +222,7 @@ import kotlinx.coroutines.launch userLocationStateFactory.create(hasLocationPermission = permissionsState.isAnyGranted) } return ShowLocationState( - customMapStyleUrl = customMapStyleUrl, + customMapTilerConfig = customMapTilerConfig, dialogState = dialogState, locationShares = locationShares, focusedLocation = focusedLocation, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt index e5ddf589b0..2ae76ef9e9 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt @@ -17,10 +17,11 @@ import io.element.android.libraries.designsystem.components.PinVariant 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.room.location.AssetType +import io.element.android.libraries.wellknown.api.MapTilerConfig import kotlinx.collections.immutable.ImmutableList data class ShowLocationState( - val customMapStyleUrl: AsyncData, + val customMapTilerConfig: AsyncData, val isLive: Boolean, val dialogState: LocationConstraintsDialogState, val locationShares: ImmutableList, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt index cd7ea108bb..a1f349d644 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt @@ -18,6 +18,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize 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.room.location.AssetType +import io.element.android.libraries.wellknown.api.MapTilerConfig import kotlinx.collections.immutable.toImmutableList class ShowLocationStateProvider : PreviewParameterProvider { @@ -36,14 +37,14 @@ class ShowLocationStateProvider : PreviewParameterProvider { constraintsDialogState = LocationConstraintsDialogState.LocationServiceDisabled, ), aShowLocationState(isTrackMyLocation = true), - aShowLocationState(customMapStyleUrl = AsyncData.Loading()), + aShowLocationState(customMapTilerConfig = AsyncData.Loading()), ) } private const val APP_NAME = "ApplicationName" fun aShowLocationState( - customMapStyleUrl: AsyncData = AsyncData.Success(null), + customMapTilerConfig: AsyncData = AsyncData.Success(null), isLive: Boolean = false, constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None, locationShares: List = listOf(aLocationShareItem(isLive = isLive)), @@ -55,7 +56,7 @@ fun aShowLocationState( eventSink: (ShowLocationEvent) -> Unit = {}, ): ShowLocationState { return ShowLocationState( - customMapStyleUrl = customMapStyleUrl, + customMapTilerConfig = customMapTilerConfig, dialogState = constraintsDialogState, locationShares = locationShares.toImmutableList(), focusedLocation = focusedLocation, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index 9fdbe3aac8..45e4c7f4cc 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -102,7 +102,7 @@ fun ShowLocationView( } } MapBottomSheetScaffold( - customMapStyleUrl = state.customMapStyleUrl, + customMapTilerConfig = state.customMapTilerConfig, sheetDragHandle = if (state.isSheetDraggable) { { BottomSheetDefaults.DragHandle() } } else { diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt index 49df684666..9be168e5a2 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt @@ -11,6 +11,7 @@ package io.element.android.features.location.impl.share import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext 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.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter @@ -58,6 +59,7 @@ class DefaultShareLocationEntryPointTest { sessionId = room.sessionId, ), userLocationStateFactory = FakeUserLocationStateFactory(), + remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(), ) }, analyticsService = FakeAnalyticsService(), diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt index f4cc135e9e..6748c5a675 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt @@ -19,6 +19,8 @@ import app.cash.turbine.ReceiveTurbine import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import im.vector.app.features.analytics.plan.Composer +import io.element.android.features.enterprise.api.remoteconfig.RemoteEnterpriseConfigProvider +import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider import io.element.android.features.location.api.Location import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.common.FakeUserLocationStateFactory @@ -30,6 +32,8 @@ 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.test.FakeActiveLiveLocationShareManager 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.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -51,6 +55,7 @@ import io.element.android.libraries.matrix.test.room.powerlevels.FakeRoomPermiss import io.element.android.libraries.matrix.test.timeline.FakeTimeline import io.element.android.libraries.preferences.api.store.PreferenceDataStoreFactory 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.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.assert @@ -60,7 +65,6 @@ import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule @@ -77,20 +81,18 @@ class ShareLocationPresenterTest { private val fakeMessageComposerContext = FakeMessageComposerContext() private val fakeLocationActions = FakeLocationActions() private val fakeBuildMeta = aBuildMeta(applicationName = "app name") - private val fakeMatrixClient = FakeMatrixClient( - sessionId = A_USER_ID, - getMapStyleUrlResult = { Result.success(null) }, - ) + private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID) private val durationFormatter = FakeDurationFormatter() - private fun TestScope.createShareLocationPresenter( + private fun createShareLocationPresenter( joinedRoom: JoinedRoom = FakeJoinedRoom(), timelineMode: Timeline.Mode = Timeline.Mode.Live, locationActions: FakeLocationActions = fakeLocationActions, liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId), client: FakeMatrixClient = fakeMatrixClient, + remoteEnterpriseConfigProvider: RemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(), ): ShareLocationPresenter = ShareLocationPresenter( permissionsPresenterFactory = { fakePermissionsPresenter }, room = joinedRoom, @@ -104,6 +106,7 @@ class ShareLocationPresenterTest { liveLocationShareManager = liveLocationShareManager, liveLocationStore = liveLocationStore, userLocationStateFactory = FakeUserLocationStateFactory(), + remoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider, ) @Test @@ -118,7 +121,7 @@ class ShareLocationPresenterTest { val shareLocationPresenter = createShareLocationPresenter() shareLocationPresenter.test { val state = awaitFirstItem() - assertThat(state.customMapStyleUrl.isLoading()).isFalse() + assertThat(state.customMapTilerConfig.isLoading()).isFalse() assertThat(state.trackUserLocation).isTrue() assertThat(state.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None)) } @@ -126,18 +129,20 @@ class ShareLocationPresenterTest { @Test fun `present - non-null customMapStyleUrl`() = runTest { + val mapTilerConfig = aMapTilerConfig(apiKey = "A KEY") val shareLocationPresenter = createShareLocationPresenter( client = FakeMatrixClient( sessionId = A_USER_ID, - getMapStyleUrlResult = { Result.success("aUrl") }, + ), + remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider( + getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = mapTilerConfig)) } ) ) shareLocationPresenter.test { - skipItems(1) val state = awaitItem() - assertThat(state.customMapStyleUrl.isLoading()).isTrue() + assertThat(state.customMapTilerConfig.isLoading()).isTrue() val finalState = awaitItem() - assertThat(finalState.customMapStyleUrl.dataOrNull()).isEqualTo("aUrl") + assertThat(finalState.customMapTilerConfig.dataOrNull()).isEqualTo(mapTilerConfig) } } @@ -709,7 +714,7 @@ class ShareLocationPresenterTest { } private suspend fun ReceiveTurbine.awaitFirstItem(): T { - skipItems(2) + skipItems(1) return awaitItem() } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt index 23b7e9bf4d..69451eedb7 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/DefaultShowLocationEntryPointTest.kt @@ -11,6 +11,7 @@ package io.element.android.features.location.impl.show import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.bumble.appyx.core.modality.BuildContext 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.ShowLocationEntryPoint import io.element.android.features.location.api.ShowLocationMode @@ -20,7 +21,7 @@ import io.element.android.features.location.impl.common.permissions.FakePermissi import io.element.android.features.location.test.FakeActiveLiveLocationShareManager import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.test.FakeMatrixClient +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.room.FakeJoinedRoom import io.element.android.services.analytics.test.FakeAnalyticsService @@ -41,8 +42,8 @@ class DefaultShowLocationEntryPointTest { ShowLocationNode( buildContext = buildContext, plugins = plugins, - presenterFactory = object : ShowLocationPresenter.Factory { - override fun create(mode: ShowLocationMode) = ShowLocationPresenter( + presenterFactory = { mode -> + ShowLocationPresenter( mode = mode, permissionsPresenterFactory = { FakePermissionsPresenter() }, locationActions = FakeLocationActions(), @@ -50,9 +51,10 @@ class DefaultShowLocationEntryPointTest { dateFormatter = FakeDateFormatter(), stringProvider = FakeStringProvider(), joinedRoom = joinedRoom, - client = FakeMatrixClient(), + sessionId = A_SESSION_ID, liveLocationShareManager = FakeActiveLiveLocationShareManager(), userLocationStateFactory = FakeUserLocationStateFactory(), + remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(), ) }, analyticsService = FakeAnalyticsService(), diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt index b579a79e4f..77b26eb66c 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt @@ -12,6 +12,7 @@ import app.cash.molecule.RecompositionMode import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.test.remoteconfig.FakeRemoteEnterpriseConfigProvider import io.element.android.features.location.api.Location import io.element.android.features.location.api.ShowLocationMode import io.element.android.features.location.impl.aPermissionsState @@ -22,17 +23,20 @@ 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.ui.LocationConstraintsDialogState 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.matrix.api.MatrixClient +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.room.JoinedRoom 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.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.matrix.test.room.FakeJoinedRoom 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.tests.testutils.WarmUpRule import io.element.android.tests.testutils.test @@ -65,8 +69,9 @@ class ShowLocationPresenterTest { ), locationActions: FakeLocationActions = fakeLocationActions, joinedRoom: JoinedRoom = FakeJoinedRoom(), - client: MatrixClient = FakeMatrixClient(), + sessionId: SessionId = A_SESSION_ID, liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), + remoteEnterpriseConfigProvider: FakeRemoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider(), ) = ShowLocationPresenter( mode = mode, permissionsPresenterFactory = { fakePermissionsPresenter }, @@ -75,24 +80,26 @@ class ShowLocationPresenterTest { dateFormatter = fakeDateFormatter, stringProvider = FakeStringProvider(), joinedRoom = joinedRoom, - client = client, + sessionId = sessionId, liveLocationShareManager = liveLocationShareManager, userLocationStateFactory = FakeUserLocationStateFactory(), + remoteEnterpriseConfigProvider = remoteEnterpriseConfigProvider, ) @Test fun `present - non-null customMapStyleUrl`() = runTest { + val mapTilerConfig = aMapTilerConfig(apiKey = "A KEY") val shareLocationPresenter = createShowLocationPresenter( - client = FakeMatrixClient( - sessionId = A_USER_ID, - getMapStyleUrlResult = { Result.success("aUrl") }, + sessionId = A_SESSION_ID, + remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider( + getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = mapTilerConfig)) } ) ) shareLocationPresenter.test { val state = awaitItem() - assertThat(state.customMapStyleUrl.isLoading()).isTrue() + assertThat(state.customMapTilerConfig.isLoading()).isTrue() val finalState = awaitItem() - assertThat(finalState.customMapStyleUrl.dataOrNull()).isEqualTo("aUrl") + assertThat(finalState.customMapTilerConfig.dataOrNull()).isEqualTo(mapTilerConfig) } } @@ -115,6 +122,8 @@ class ShowLocationPresenterTest { initialState.eventSink(ShowLocationEvent.Share(location)) assertThat(fakeLocationActions.sharedLocation).isEqualTo(location) + + cancelAndIgnoreRemainingEvents() } } @@ -123,10 +132,12 @@ class ShowLocationPresenterTest { fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) val presenter = createShowLocationPresenter( - client = FakeMatrixClient( - getMapStyleUrlResult = { Result.success(null) } + sessionId = A_SESSION_ID, + remoteEnterpriseConfigProvider = FakeRemoteEnterpriseConfigProvider( + getResult = { WellknownRetrieverResult.Success(anElementWellKnown(mapTilerConfig = aMapTilerConfig())) } ) ) + presenter.test { val initialState = awaitItem() assertThat(initialState.isTrackMyLocation).isFalse() @@ -281,6 +292,8 @@ class ShowLocationPresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.appName).isEqualTo("app name") + + cancelAndIgnoreRemainingEvents() } } @@ -334,6 +347,8 @@ class ShowLocationPresenterTest { val initialState = awaitItem() assertThat(initialState.locationShares).isEmpty() assertThat(initialState.isSheetDraggable).isFalse() + + cancelAndIgnoreRemainingEvents() } } @@ -408,6 +423,9 @@ class ShowLocationPresenterTest { val initialState = awaitItem() assertThat(initialState.locationShares).isEmpty() + // Wait until we receive the custom map style URL result + skipItems(1) + // Emit a new live share liveSharesFlow.value = listOf( aLiveLocationShare(userId = userId) @@ -447,6 +465,8 @@ class ShowLocationPresenterTest { assertThat(item.avatarData.id).isEqualTo(senderId.value) assertThat(item.avatarData.name).isEqualTo(senderName) assertThat(item.avatarData.url).isEqualTo(avatarUrl) + + cancelAndIgnoreRemainingEvents() } } @@ -456,6 +476,7 @@ class ShowLocationPresenterTest { presenter.test { val state = awaitItem() assertThat(state.isSheetDraggable).isFalse() + cancelAndIgnoreRemainingEvents() } } @@ -465,6 +486,8 @@ class ShowLocationPresenterTest { presenter.test { val state = awaitItem() assertThat(state.hideUserLocationPuck).isFalse() + + cancelAndIgnoreRemainingEvents() } } @@ -476,6 +499,8 @@ class ShowLocationPresenterTest { presenter.test { val state = awaitItem() assertThat(state.hideUserLocationPuck).isFalse() + + cancelAndIgnoreRemainingEvents() } } diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index b40aebab39..0b51a2b80d 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -61,6 +61,7 @@ dependencies { implementation(projects.libraries.push.api) implementation(projects.libraries.uiUtils) implementation(projects.libraries.testtags) + implementation(projects.libraries.wellknown.api) implementation(projects.features.networkmonitor.api) implementation(projects.features.contentscanner.api) implementation(projects.services.analytics.compose) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt index 4ab4ee84a6..a7f7b33989 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.features.location.api.LocalMapTilerConfig import io.element.android.features.location.api.StaticMapView import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentProvider @@ -50,6 +51,7 @@ fun TimelineItemLocationView( modifier = Modifier .fillMaxWidth() .heightIn(max = 188.dp), + mapTilerConfig = LocalMapTilerConfig.current, pinVariant = content.pinVariant, location = content.location, zoom = 15.0, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index e3aa2004bf..26d7d02016 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -255,11 +255,6 @@ interface MatrixClient : UrlContentFetcher { */ suspend fun performDatabaseVacuum(): Result - /** - * Returns the URL of the map style configured on the server, if any. - */ - suspend fun getMapStyleUrl(): Result - /** * Resets the cached client `well-known` config by the SDK. */ diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 7aacb8edc6..0ff563a204 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -886,12 +886,6 @@ class RustMatrixClient( } } - override suspend fun getMapStyleUrl(): Result = withContext(sessionDispatcher) { - runCatchingExceptions { - innerClient.tileServer()?.mapStyleUrl - } - } - override suspend fun resetWellKnownConfig(): Result { return runCatchingExceptions { Timber.d("Resetting well-known config for session $sessionId") diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 60f70c12c5..56d25ed175 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -129,7 +129,6 @@ class FakeMatrixClient( private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result = { _, _ -> lambdaError() }, private val markAllRoomsAsReadResult: () -> Result = { Result.success(Unit) }, private val performDatabaseVacuumLambda: () -> Result = { lambdaError() }, - private val getMapStyleUrlResult: () -> Result = { lambdaError() }, private val getDatabaseSizesLambda: () -> Result = { lambdaError() }, private val resetWellKnownConfigLambda: () -> Result = { lambdaError() }, private val enableAutomaticCallStatusLambda: (Boolean) -> Unit = { }, @@ -435,10 +434,6 @@ class FakeMatrixClient( return performDatabaseVacuumLambda() } - override suspend fun getMapStyleUrl(): Result = simulateLongTask { - getMapStyleUrlResult() - } - override suspend fun canLinkNewDevice(): Result = simulateLongTask { return canLinkNewDeviceResult() } diff --git a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt index a9c42b7121..88a89e7a49 100644 --- a/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt +++ b/libraries/wellknown/api/src/main/kotlin/io/element/android/libraries/wellknown/api/ElementWellKnown.kt @@ -18,4 +18,19 @@ data class ElementWellKnown( 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. + */ +data class MapTilerConfig( + /** The API key to use with MapTiler. This is always not null, but can be empty/blank. */ + val apiKey: String, + /** The style ID to use for light mode. This is optional and can be null. */ + val lightStyleId: String?, + /** The style ID to use for light mode. This is optional and can be null. */ + val darkStyleId: String?, + /** The base URL to use for MapTiler. This is optional and can be null. */ + val baseUrl: String?, ) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt index c585264f55..317109a1c0 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt @@ -48,6 +48,8 @@ data class InternalElementWellKnown( val contentScannerUrl: String? = null, @SerialName("force_disable_e2ee") val forceDisableE2EE: Boolean? = null, + @SerialName("map_tiler") + val mapTilerConfig: InternalMapTilerConfig? = null, ) @Serializable @@ -55,3 +57,15 @@ 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, +) diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt index c4d15e7325..254036c5c3 100644 --- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt +++ b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt @@ -11,6 +11,7 @@ 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") @@ -32,6 +33,14 @@ fun InternalElementWellKnown.map() = ElementWellKnown( 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 { diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt index 224c847387..a58aa0a347 100644 --- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt +++ b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt @@ -13,6 +13,7 @@ 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 @@ -204,7 +205,11 @@ class DefaultWellknownRetrieverTest { "notification_sound": "a_notification_sound.flac", "idp_app_scheme": "an_app_scheme", "content_scanner_url": "https://content-scanner.example.com", - "force_disable_e2ee": false + "force_disable_e2ee": false, + "map_tiler": { + "api_key": "test_api_key", + "base_url": "https://tile-server.example.com" + } }""" private val expectedElementWellKnown = ElementWellKnown( @@ -217,6 +222,10 @@ class DefaultWellknownRetrieverTest { contentScannerUrl = "https://content-scanner.example.com", customRecoveryPassphrase = null, forceDisableE2EE = false, + mapTilerConfig = aMapTilerConfig( + apiKey = "test_api_key", + baseUrl = "https://tile-server.example.com", + ) ) } } diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt index 941e5f4cc7..36d9f7bc82 100644 --- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt +++ b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt @@ -10,6 +10,7 @@ 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, @@ -21,6 +22,7 @@ fun anElementWellKnown( customRecoveryPassphrase: CustomRecoveryPassphrase? = null, contentScannerUrl: String? = null, forceDisableE2EE: Boolean? = null, + mapTilerConfig: MapTilerConfig? = null, ) = ElementWellKnown( registrationHelperUrl = registrationHelperUrl, enforceElementPro = enforceElementPro, @@ -31,6 +33,7 @@ fun anElementWellKnown( customRecoveryPassphrase = customRecoveryPassphrase, contentScannerUrl = contentScannerUrl, forceDisableE2EE = forceDisableE2EE, + mapTilerConfig = mapTilerConfig, ) fun aCustomRecoveryPassphrase( @@ -38,3 +41,15 @@ fun aCustomRecoveryPassphrase( ) = 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, +) diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index 79bfeb7250..147eca01d0 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -230,6 +230,7 @@ Compose: - LocalEventContentValidationState - LocalSnackbarDispatcher - LocalCameraPositionState + - LocalMapTilerConfig - LocalMediaItemPresenterFactories - LocalTimelineItemPresenterFactories - LocalRoomMemberProfilesCache