From 0149e6f65b86e28f6c608270b46f0826e84c291c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Oct 2026 10:11:07 +0200 Subject: [PATCH] Do not mix the homeserver MapTiler config with the build time one The map URL builders now only use the MapTilerConfig they are given, falling back to the public MapTiler defaults for the missing values, instead of the values defined at build time. This avoids for instance sending the build time API key to a tile server provided by the homeserver. When there is no config, no static map URL builder is created, so no request is performed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../features/location/api/StaticMapView.kt | 6 +- .../location/api/internal/MapTilerDefaults.kt | 33 ++++++++++ .../internal/MapTilerStaticMapUrlBuilder.kt | 16 ++--- .../MapTilerTileServerStyleUriBuilder.kt | 27 ++------ .../api/internal/StaticMapUrlBuilder.kt | 10 +-- .../api/internal/TileServerStyleUriBuilder.kt | 19 +++--- .../MapTilerStaticMapUrlBuilderTest.kt | 33 ++++++++++ .../MapTilerTileServerStyleUriBuilderTest.kt | 61 ++++++++----------- 8 files changed, 118 insertions(+), 87 deletions(-) create mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerDefaults.kt 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 f6a70f6653..2e8e8b5a60 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 @@ -152,8 +152,8 @@ private fun BoxWithConstraintsScope.LoadableMapContent( Triple(painter, state, ContentScale.Crop) } else { val painter = rememberAsyncImagePainter( - model = if (constraints.isZero) { - // Avoid building a URL if any of the size constraints is zero + model = if (builder == null || constraints.isZero) { + // Avoid building a URL if there is no map provider or if any of the size constraints is zero null } else { ImageRequest.Builder(context) @@ -195,7 +195,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent( else -> { StaticMapPlaceholder( painter = painterResource(R.drawable.blurred_map), - canReload = builder.isServiceAvailable() && state is AsyncImagePainter.State.Error, + canReload = builder?.isServiceAvailable() == true && state is AsyncImagePainter.State.Error, contentDescription = contentDescription, width = maxWidth, height = maxHeight, diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerDefaults.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerDefaults.kt new file mode 100644 index 0000000000..509278b3dd --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapTilerDefaults.kt @@ -0,0 +1,33 @@ +/* + * 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.internal + +import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig + +/** + * Public MapTiler defaults, used when a [MapTilerConfig] does not define a value. + * Values from another configuration (e.g. the build time one) must never be used as a fallback, + * to avoid sending an API key to a server it was not meant for. + */ +internal object MapTilerDefaults { + const val BASE_URL = "https://api.maptiler.com/maps" + const val LIGHT_MAP_ID = "basic-v2" + const val DARK_MAP_ID = "basic-v2-dark" +} + +internal fun MapTilerConfig.baseUrlOrDefault(): String { + return baseUrl?.takeIf { it.isNotBlank() }?.removeSuffix("/") ?: MapTilerDefaults.BASE_URL +} + +internal fun MapTilerConfig.lightMapIdOrDefault(): String { + return lightStyleId?.takeIf { it.isNotBlank() } ?: MapTilerDefaults.LIGHT_MAP_ID +} + +internal fun MapTilerConfig.darkMapIdOrDefault(): String { + return darkStyleId?.takeIf { it.isNotBlank() } ?: MapTilerDefaults.DARK_MAP_ID +} 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 cd339f67b0..c19d305f3d 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,7 +9,6 @@ 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 kotlin.math.roundToInt /** @@ -23,18 +22,11 @@ internal class MapTilerStaticMapUrlBuilder( private val lightMapId: String, private val darkMapId: String, ) : StaticMapUrlBuilder { - constructor() : this( - baseUrl = BuildConfig.MAPTILER_BASE_URL.removeSuffix("/"), - apiKey = BuildConfig.MAPTILER_API_KEY, - lightMapId = BuildConfig.MAPTILER_LIGHT_MAP_ID, - 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, + baseUrl = mapTilerConfig.baseUrlOrDefault(), + apiKey = mapTilerConfig.apiKey, + lightMapId = mapTilerConfig.lightMapIdOrDefault(), + darkMapId = mapTilerConfig.darkMapIdOrDefault(), ) override fun build( 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 d817a3307f..fc7ddc3fe0 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,33 +11,18 @@ 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 - -internal class MapTilerTileServerStyleUriBuilder( - private val baseUrl: String, - private val apiKey: String, - private val lightMapId: String, - private val darkMapId: String, -) : TileServerStyleUriBuilder { - constructor() : this( - baseUrl = BuildConfig.MAPTILER_BASE_URL.removeSuffix("/"), - apiKey = BuildConfig.MAPTILER_API_KEY, - lightMapId = BuildConfig.MAPTILER_LIGHT_MAP_ID, - darkMapId = BuildConfig.MAPTILER_DARK_MAP_ID, - ) +internal class MapTilerTileServerStyleUriBuilder : TileServerStyleUriBuilder { override fun build( - customMapTilerConfig: MapTilerConfig?, + mapTilerConfig: MapTilerConfig, darkMode: Boolean, ): String { - val baseUrl = customMapTilerConfig?.baseUrl.takeIf { !it.isNullOrBlank() } ?: baseUrl - val apiKey = customMapTilerConfig?.apiKey ?: apiKey + val baseUrl = mapTilerConfig.baseUrlOrDefault() val mapId = if (darkMode) { - customMapTilerConfig?.darkStyleId ?: darkMapId + mapTilerConfig.darkMapIdOrDefault() } else { - customMapTilerConfig?.lightStyleId ?: lightMapId + mapTilerConfig.lightMapIdOrDefault() } - - return "$baseUrl/$mapId/style.json?key=$apiKey" + return "$baseUrl/$mapId/style.json?key=${mapTilerConfig.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 31992b1dd6..553a4ed160 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 @@ -36,17 +36,13 @@ interface StaticMapUrlBuilder { density: Float, ): String - /** Whether a map provider is configured for this build; the URLs are unusable when it is not. */ + /** Whether an API key is configured; the URLs are unusable when it is not. */ fun isServiceAvailable(): Boolean } @Composable -fun rememberStaticMapBuilder(mapTilerConfig: MapTilerConfig?): StaticMapUrlBuilder { +fun rememberStaticMapBuilder(mapTilerConfig: MapTilerConfig?): StaticMapUrlBuilder? { return remember(mapTilerConfig) { - if (mapTilerConfig != null) { - MapTilerStaticMapUrlBuilder(mapTilerConfig) - } else { - MapTilerStaticMapUrlBuilder() - } + mapTilerConfig?.let { MapTilerStaticMapUrlBuilder(it) } } } 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 67936525e5..6195f8fad3 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 @@ -22,11 +22,11 @@ interface TileServerStyleUriBuilder { /** * Builds the style URI the map view loads its tiles from. * - * @param customMapTilerConfig the custom MapTiler configuration received from the server, or null if not available. + * @param mapTilerConfig the MapTiler configuration to use. * @param darkMode whether to request the dark map style. */ fun build( - customMapTilerConfig: MapTilerConfig?, + mapTilerConfig: MapTilerConfig, darkMode: Boolean, ): String } @@ -38,13 +38,16 @@ interface TileServerStyleUriBuilder { */ @Composable fun rememberTileStyleUrl( - customMapTileConfig: MapTilerConfig?, + mapTilerConfig: MapTilerConfig?, ): String { val darkMode = !ElementTheme.isLightTheme - return remember(darkMode, customMapTileConfig) { - MapTilerTileServerStyleUriBuilder().build( - customMapTilerConfig = customMapTileConfig, - darkMode = darkMode, - ) + return remember(darkMode, mapTilerConfig) { + // Without config, the map is not displayed, so the style URL is not used. + mapTilerConfig?.let { + MapTilerTileServerStyleUriBuilder().build( + mapTilerConfig = it, + darkMode = darkMode, + ) + }.orEmpty() } } diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt index 3d17107104..c46b7a41d5 100644 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt +++ b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/MapTilerStaticMapUrlBuilderTest.kt @@ -9,6 +9,7 @@ package io.element.android.features.location.api.internal import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig import org.junit.Test class MapTilerStaticMapUrlBuilderTest { @@ -24,6 +25,38 @@ class MapTilerStaticMapUrlBuilderTest { assertThat(builder.isServiceAvailable()).isTrue() } + @Test + fun `builder created from a config only uses the config values`() { + val configBuilder = MapTilerStaticMapUrlBuilder( + MapTilerConfig( + apiKey = "API_KEY", + lightStyleId = "light", + darkStyleId = "dark", + baseUrl = "https://custom.url/maps/", + ) + ) + assertThat(configBuilder.isServiceAvailable()).isTrue() + assertThat( + configBuilder.build(lat = 1.23, lon = -4.56, zoom = 7.8, darkMode = true, width = 800, height = 600, density = 1f) + ).isEqualTo("https://custom.url/maps/dark/static/-4.56,1.23,7.8/800x600.webp?key=API_KEY&attribution=topright") + } + + @Test + fun `builder created from a partial config falls back to the public MapTiler defaults`() { + val configBuilder = MapTilerStaticMapUrlBuilder( + MapTilerConfig( + apiKey = "", + lightStyleId = null, + darkStyleId = null, + baseUrl = null, + ) + ) + assertThat(configBuilder.isServiceAvailable()).isFalse() + assertThat( + configBuilder.build(lat = 1.23, lon = -4.56, zoom = 7.8, darkMode = false, width = 800, height = 600, density = 1f) + ).isEqualTo("https://api.maptiler.com/maps/basic-v2/static/-4.56,1.23,7.8/800x600.webp?key=&attribution=topright") + } + @Test fun `isServiceAvailable returns false if api key is empty`() { val builderWithoutKey = MapTilerStaticMapUrlBuilder( 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 c153ab1ce4..12c3a7ec63 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 @@ -13,75 +13,64 @@ import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig import org.junit.Test class MapTilerTileServerStyleUriBuilderTest { - private val builder = MapTilerTileServerStyleUriBuilder( - baseUrl = "https://base.url", - apiKey = "anApiKey", - lightMapId = "aLightMapId", - darkMapId = "aDarkMapId", + private val builder = MapTilerTileServerStyleUriBuilder() + + private val config = MapTilerConfig( + apiKey = "API_KEY", + lightStyleId = "light", + darkStyleId = "dark", + baseUrl = "https://custom.url/maps/", ) @Test fun `light map uri`() { assertThat( builder.build( - customMapTilerConfig = null, + mapTilerConfig = config, darkMode = false, ) - ).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey") + ).isEqualTo("https://custom.url/maps/light/style.json?key=API_KEY") } @Test fun `dark map uri`() { assertThat( builder.build( - customMapTilerConfig = null, + mapTilerConfig = config, darkMode = true, ) - ).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey") + ).isEqualTo("https://custom.url/maps/dark/style.json?key=API_KEY") } @Test - fun `custom map uri light`() { + fun `missing values fall back to the public MapTiler defaults`() { + val partialConfig = MapTilerConfig( + apiKey = "API_KEY", + lightStyleId = null, + darkStyleId = " ", + baseUrl = " ", + ) assertThat( builder.build( - customMapTilerConfig = MapTilerConfig( - apiKey = "API_KEY", - lightStyleId = "light", - darkStyleId = "dark", - baseUrl = "https://custom.url/style.json" - ), + mapTilerConfig = partialConfig, darkMode = false, ) - ).isEqualTo("https://custom.url/style.json/light/style.json?key=API_KEY") - } - - @Test - fun `custom map uri dark`() { + ).isEqualTo("https://api.maptiler.com/maps/basic-v2/style.json?key=API_KEY") assertThat( builder.build( - customMapTilerConfig = MapTilerConfig( - apiKey = "API_KEY", - lightStyleId = "light", - darkStyleId = "dark", - baseUrl = "https://custom.url/style.json" - ), + mapTilerConfig = partialConfig, darkMode = true, ) - ).isEqualTo("https://custom.url/style.json/dark/style.json?key=API_KEY") + ).isEqualTo("https://api.maptiler.com/maps/basic-v2-dark/style.json?key=API_KEY") } @Test - fun `blank custom map uri falls back to the built-in style`() { + fun `blank api key is kept as is`() { assertThat( builder.build( - customMapTilerConfig = MapTilerConfig( - apiKey = "API_KEY", - lightStyleId = "light", - darkStyleId = "dark", - baseUrl = " " - ), + mapTilerConfig = config.copy(apiKey = ""), darkMode = false, ) - ).isEqualTo("https://base.url/light/style.json?key=API_KEY") + ).isEqualTo("https://custom.url/maps/light/style.json?key=") } }