Merge pull request #7915 from element-hq/feature/bma/doNotMixMapTilerConfigs

Do not mix the homeserver MapTiler config with the build time one
This commit is contained in:
Benoit Marty
2026-10-14 14:19:22 +02:00
committed by GitHub
12 changed files with 138 additions and 111 deletions
+4 -9
View File
@@ -26,7 +26,7 @@ android {
defaultConfig { defaultConfig {
buildConfigFieldStr( buildConfigFieldStr(
name = "MAPTILER_BASE_URL", name = "MAPTILER_BASE_URL",
value = BuildTimeConfig.SERVICES_MAPTILER_BASE_URL ?: "https://api.maptiler.com/maps" value = BuildTimeConfig.SERVICES_MAPTILER_BASE_URL,
) )
buildConfigFieldStr( buildConfigFieldStr(
name = "MAPTILER_API_KEY", name = "MAPTILER_API_KEY",
@@ -35,8 +35,7 @@ android {
} else { } else {
System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY") System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY")
?: readLocalProperty("services.maptiler.apikey") ?: readLocalProperty("services.maptiler.apikey")
} },
?: ""
) )
buildConfigFieldStr( buildConfigFieldStr(
name = "MAPTILER_LIGHT_MAP_ID", name = "MAPTILER_LIGHT_MAP_ID",
@@ -45,9 +44,7 @@ android {
} else { } else {
System.getenv("ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID") System.getenv("ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID")
?: readLocalProperty("services.maptiler.lightMapId") ?: readLocalProperty("services.maptiler.lightMapId")
} },
// fall back to maptiler's default light map.
?: "basic-v2"
) )
buildConfigFieldStr( buildConfigFieldStr(
name = "MAPTILER_DARK_MAP_ID", name = "MAPTILER_DARK_MAP_ID",
@@ -56,9 +53,7 @@ android {
} else { } else {
System.getenv("ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID") System.getenv("ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID")
?: readLocalProperty("services.maptiler.darkMapId") ?: readLocalProperty("services.maptiler.darkMapId")
} },
// fall back to maptiler's default dark map.
?: "basic-v2-dark"
) )
} }
} }
@@ -19,14 +19,13 @@ val LocalMapTilerConfig = staticCompositionLocalOf { buildTimeMapTilerConfig() }
* The [MapTilerConfig] defined at build time, or null if no MapTiler API key has been provided. * The [MapTilerConfig] defined at build time, or null if no MapTiler API key has been provided.
*/ */
fun buildTimeMapTilerConfig(): MapTilerConfig? { fun buildTimeMapTilerConfig(): MapTilerConfig? {
return BuildConfig.MAPTILER_API_KEY // BuildConfig fields are null when not provided at build time.
.takeIf { it.isNotEmpty() } val apiKey: String? = BuildConfig.MAPTILER_API_KEY
?.let { apiKey -> if (apiKey.isNullOrEmpty()) return null
MapTilerConfig( return MapTilerConfig(
apiKey = apiKey, apiKey = apiKey,
lightStyleId = BuildConfig.MAPTILER_LIGHT_MAP_ID, lightStyleId = BuildConfig.MAPTILER_LIGHT_MAP_ID,
darkStyleId = BuildConfig.MAPTILER_DARK_MAP_ID, darkStyleId = BuildConfig.MAPTILER_DARK_MAP_ID,
baseUrl = BuildConfig.MAPTILER_BASE_URL baseUrl = BuildConfig.MAPTILER_BASE_URL,
) )
}
} }
@@ -152,8 +152,8 @@ private fun BoxWithConstraintsScope.LoadableMapContent(
Triple(painter, state, ContentScale.Crop) Triple(painter, state, ContentScale.Crop)
} else { } else {
val painter = rememberAsyncImagePainter( val painter = rememberAsyncImagePainter(
model = if (constraints.isZero) { model = if (builder == null || constraints.isZero) {
// Avoid building a URL if any of the size constraints is zero // Avoid building a URL if there is no map provider or if any of the size constraints is zero
null null
} else { } else {
ImageRequest.Builder(context) ImageRequest.Builder(context)
@@ -195,7 +195,7 @@ private fun BoxWithConstraintsScope.LoadableMapContent(
else -> { else -> {
StaticMapPlaceholder( StaticMapPlaceholder(
painter = painterResource(R.drawable.blurred_map), 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, contentDescription = contentDescription,
width = maxWidth, width = maxWidth,
height = maxHeight, height = maxHeight,
@@ -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
}
@@ -9,7 +9,6 @@
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.enterprise.api.remoteconfig.MapTilerConfig
import io.element.android.features.location.api.BuildConfig
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** /**
@@ -23,18 +22,11 @@ internal class MapTilerStaticMapUrlBuilder(
private val lightMapId: String, private val lightMapId: String,
private val darkMapId: String, private val darkMapId: String,
) : StaticMapUrlBuilder { ) : 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( constructor(mapTilerConfig: MapTilerConfig) : this(
baseUrl = (mapTilerConfig.baseUrl ?: BuildConfig.MAPTILER_BASE_URL).removeSuffix("/"), baseUrl = mapTilerConfig.baseUrlOrDefault(),
apiKey = mapTilerConfig.apiKey.takeIf { it.isNotBlank() } ?: BuildConfig.MAPTILER_API_KEY, apiKey = mapTilerConfig.apiKey,
lightMapId = mapTilerConfig.lightStyleId ?: BuildConfig.MAPTILER_LIGHT_MAP_ID, lightMapId = mapTilerConfig.lightMapIdOrDefault(),
darkMapId = mapTilerConfig.darkStyleId ?: BuildConfig.MAPTILER_DARK_MAP_ID, darkMapId = mapTilerConfig.darkMapIdOrDefault(),
) )
override fun build( override fun build(
@@ -11,33 +11,18 @@
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.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( override fun build(
customMapTilerConfig: MapTilerConfig?, mapTilerConfig: MapTilerConfig,
darkMode: Boolean, darkMode: Boolean,
): String { ): String {
val baseUrl = customMapTilerConfig?.baseUrl.takeIf { !it.isNullOrBlank() } ?: baseUrl val baseUrl = mapTilerConfig.baseUrlOrDefault()
val apiKey = customMapTilerConfig?.apiKey ?: apiKey
val mapId = if (darkMode) { val mapId = if (darkMode) {
customMapTilerConfig?.darkStyleId ?: darkMapId mapTilerConfig.darkMapIdOrDefault()
} else { } else {
customMapTilerConfig?.lightStyleId ?: lightMapId mapTilerConfig.lightMapIdOrDefault()
} }
return "$baseUrl/$mapId/style.json?key=${mapTilerConfig.apiKey}"
return "$baseUrl/$mapId/style.json?key=$apiKey"
} }
} }
@@ -36,17 +36,13 @@ interface StaticMapUrlBuilder {
density: Float, density: Float,
): String ): 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 fun isServiceAvailable(): Boolean
} }
@Composable @Composable
fun rememberStaticMapBuilder(mapTilerConfig: MapTilerConfig?): StaticMapUrlBuilder { fun rememberStaticMapBuilder(mapTilerConfig: MapTilerConfig?): StaticMapUrlBuilder? {
return remember(mapTilerConfig) { return remember(mapTilerConfig) {
if (mapTilerConfig != null) { mapTilerConfig?.let { MapTilerStaticMapUrlBuilder(it) }
MapTilerStaticMapUrlBuilder(mapTilerConfig)
} else {
MapTilerStaticMapUrlBuilder()
}
} }
} }
@@ -22,11 +22,11 @@ interface TileServerStyleUriBuilder {
/** /**
* Builds the style URI the map view loads its tiles from. * 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. * @param darkMode whether to request the dark map style.
*/ */
fun build( fun build(
customMapTilerConfig: MapTilerConfig?, mapTilerConfig: MapTilerConfig,
darkMode: Boolean, darkMode: Boolean,
): String ): String
} }
@@ -38,13 +38,16 @@ interface TileServerStyleUriBuilder {
*/ */
@Composable @Composable
fun rememberTileStyleUrl( fun rememberTileStyleUrl(
customMapTileConfig: MapTilerConfig?, mapTilerConfig: MapTilerConfig?,
): String { ): String {
val darkMode = !ElementTheme.isLightTheme val darkMode = !ElementTheme.isLightTheme
return remember(darkMode, customMapTileConfig) { return remember(darkMode, mapTilerConfig) {
MapTilerTileServerStyleUriBuilder().build( // Without config, the map is not displayed, so the style URL is not used.
customMapTilerConfig = customMapTileConfig, mapTilerConfig?.let {
darkMode = darkMode, MapTilerTileServerStyleUriBuilder().build(
) mapTilerConfig = it,
darkMode = darkMode,
)
}.orEmpty()
} }
} }
@@ -9,6 +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.features.enterprise.api.remoteconfig.MapTilerConfig
import org.junit.Test import org.junit.Test
class MapTilerStaticMapUrlBuilderTest { class MapTilerStaticMapUrlBuilderTest {
@@ -24,6 +25,38 @@ class MapTilerStaticMapUrlBuilderTest {
assertThat(builder.isServiceAvailable()).isTrue() 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 @Test
fun `isServiceAvailable returns false if api key is empty`() { fun `isServiceAvailable returns false if api key is empty`() {
val builderWithoutKey = MapTilerStaticMapUrlBuilder( val builderWithoutKey = MapTilerStaticMapUrlBuilder(
@@ -13,75 +13,64 @@ import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig
import org.junit.Test import org.junit.Test
class MapTilerTileServerStyleUriBuilderTest { class MapTilerTileServerStyleUriBuilderTest {
private val builder = MapTilerTileServerStyleUriBuilder( private val builder = MapTilerTileServerStyleUriBuilder()
baseUrl = "https://base.url",
apiKey = "anApiKey", private val config = MapTilerConfig(
lightMapId = "aLightMapId", apiKey = "API_KEY",
darkMapId = "aDarkMapId", lightStyleId = "light",
darkStyleId = "dark",
baseUrl = "https://custom.url/maps/",
) )
@Test @Test
fun `light map uri`() { fun `light map uri`() {
assertThat( assertThat(
builder.build( builder.build(
customMapTilerConfig = null, mapTilerConfig = config,
darkMode = false, darkMode = false,
) )
).isEqualTo("https://base.url/aLightMapId/style.json?key=anApiKey") ).isEqualTo("https://custom.url/maps/light/style.json?key=API_KEY")
} }
@Test @Test
fun `dark map uri`() { fun `dark map uri`() {
assertThat( assertThat(
builder.build( builder.build(
customMapTilerConfig = null, mapTilerConfig = config,
darkMode = true, darkMode = true,
) )
).isEqualTo("https://base.url/aDarkMapId/style.json?key=anApiKey") ).isEqualTo("https://custom.url/maps/dark/style.json?key=API_KEY")
} }
@Test @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( assertThat(
builder.build( builder.build(
customMapTilerConfig = MapTilerConfig( mapTilerConfig = partialConfig,
apiKey = "API_KEY",
lightStyleId = "light",
darkStyleId = "dark",
baseUrl = "https://custom.url/style.json"
),
darkMode = false, darkMode = false,
) )
).isEqualTo("https://custom.url/style.json/light/style.json?key=API_KEY") ).isEqualTo("https://api.maptiler.com/maps/basic-v2/style.json?key=API_KEY")
}
@Test
fun `custom map uri dark`() {
assertThat( assertThat(
builder.build( builder.build(
customMapTilerConfig = MapTilerConfig( mapTilerConfig = partialConfig,
apiKey = "API_KEY",
lightStyleId = "light",
darkStyleId = "dark",
baseUrl = "https://custom.url/style.json"
),
darkMode = true, 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 @Test
fun `blank custom map uri falls back to the built-in style`() { fun `blank api key is kept as is`() {
assertThat( assertThat(
builder.build( builder.build(
customMapTilerConfig = MapTilerConfig( mapTilerConfig = config.copy(apiKey = ""),
apiKey = "API_KEY",
lightStyleId = "light",
darkStyleId = "dark",
baseUrl = " "
),
darkMode = false, darkMode = false,
) )
).isEqualTo("https://base.url/light/style.json?key=API_KEY") ).isEqualTo("https://custom.url/maps/light/style.json?key=")
} }
} }
@@ -11,7 +11,6 @@ package io.element.android.features.location.impl
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig
import io.element.android.features.location.api.BuildConfig
import io.element.android.features.location.api.buildTimeMapTilerConfig import io.element.android.features.location.api.buildTimeMapTilerConfig
import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.AN_EXCEPTION
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -30,7 +29,7 @@ class DefaultLocationServiceTest {
) )
runCurrent() runCurrent()
assertThat(locationService.mapTilerConfig.value).isEqualTo(buildTimeMapTilerConfig()) assertThat(locationService.mapTilerConfig.value).isEqualTo(buildTimeMapTilerConfig())
assertThat(locationService.isServiceAvailable().value).isEqualTo(BuildConfig.MAPTILER_API_KEY.isNotEmpty()) assertThat(locationService.isServiceAvailable().value).isEqualTo(buildTimeMapTilerConfig() != null)
} }
@Test @Test
@@ -40,7 +39,7 @@ class DefaultLocationServiceTest {
) )
runCurrent() runCurrent()
assertThat(locationService.mapTilerConfig.value).isEqualTo(buildTimeMapTilerConfig()) assertThat(locationService.mapTilerConfig.value).isEqualTo(buildTimeMapTilerConfig())
assertThat(locationService.isServiceAvailable().value).isEqualTo(BuildConfig.MAPTILER_API_KEY.isNotEmpty()) assertThat(locationService.isServiceAvailable().value).isEqualTo(buildTimeMapTilerConfig() != null)
} }
@Test @Test
@@ -10,14 +10,17 @@ package extension
import com.android.build.api.dsl.VariantDimension import com.android.build.api.dsl.VariantDimension
/**
* Adds a String field to the generated BuildConfig class. If [value] is null, the field will be null.
*/
fun VariantDimension.buildConfigFieldStr( fun VariantDimension.buildConfigFieldStr(
name: String, name: String,
value: String, value: String?,
) { ) {
buildConfigField( buildConfigField(
type = "String", type = "String",
name = name, name = name,
value = "\"$value\"" value = value?.let { "\"$it\"" } ?: "null"
) )
} }