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 {
buildConfigFieldStr(
name = "MAPTILER_BASE_URL",
value = BuildTimeConfig.SERVICES_MAPTILER_BASE_URL ?: "https://api.maptiler.com/maps"
value = BuildTimeConfig.SERVICES_MAPTILER_BASE_URL,
)
buildConfigFieldStr(
name = "MAPTILER_API_KEY",
@@ -35,8 +35,7 @@ android {
} else {
System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY")
?: readLocalProperty("services.maptiler.apikey")
}
?: ""
},
)
buildConfigFieldStr(
name = "MAPTILER_LIGHT_MAP_ID",
@@ -45,9 +44,7 @@ android {
} else {
System.getenv("ELEMENT_ANDROID_MAPTILER_LIGHT_MAP_ID")
?: readLocalProperty("services.maptiler.lightMapId")
}
// fall back to maptiler's default light map.
?: "basic-v2"
},
)
buildConfigFieldStr(
name = "MAPTILER_DARK_MAP_ID",
@@ -56,9 +53,7 @@ android {
} else {
System.getenv("ELEMENT_ANDROID_MAPTILER_DARK_MAP_ID")
?: 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.
*/
fun buildTimeMapTilerConfig(): MapTilerConfig? {
return BuildConfig.MAPTILER_API_KEY
.takeIf { it.isNotEmpty() }
?.let { apiKey ->
MapTilerConfig(
apiKey = apiKey,
lightStyleId = BuildConfig.MAPTILER_LIGHT_MAP_ID,
darkStyleId = BuildConfig.MAPTILER_DARK_MAP_ID,
baseUrl = BuildConfig.MAPTILER_BASE_URL
)
}
// BuildConfig fields are null when not provided at build time.
val apiKey: String? = BuildConfig.MAPTILER_API_KEY
if (apiKey.isNullOrEmpty()) return null
return MapTilerConfig(
apiKey = apiKey,
lightStyleId = BuildConfig.MAPTILER_LIGHT_MAP_ID,
darkStyleId = BuildConfig.MAPTILER_DARK_MAP_ID,
baseUrl = BuildConfig.MAPTILER_BASE_URL,
)
}
@@ -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,
@@ -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
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(
@@ -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}"
}
}
@@ -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) }
}
}
@@ -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()
}
}
@@ -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(
@@ -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=")
}
}
@@ -11,7 +11,6 @@ package io.element.android.features.location.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider
import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig
import io.element.android.features.location.api.BuildConfig
import io.element.android.features.location.api.buildTimeMapTilerConfig
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import kotlinx.coroutines.CoroutineScope
@@ -30,7 +29,7 @@ class DefaultLocationServiceTest {
)
runCurrent()
assertThat(locationService.mapTilerConfig.value).isEqualTo(buildTimeMapTilerConfig())
assertThat(locationService.isServiceAvailable().value).isEqualTo(BuildConfig.MAPTILER_API_KEY.isNotEmpty())
assertThat(locationService.isServiceAvailable().value).isEqualTo(buildTimeMapTilerConfig() != null)
}
@Test
@@ -40,7 +39,7 @@ class DefaultLocationServiceTest {
)
runCurrent()
assertThat(locationService.mapTilerConfig.value).isEqualTo(buildTimeMapTilerConfig())
assertThat(locationService.isServiceAvailable().value).isEqualTo(BuildConfig.MAPTILER_API_KEY.isNotEmpty())
assertThat(locationService.isServiceAvailable().value).isEqualTo(buildTimeMapTilerConfig() != null)
}
@Test
@@ -10,14 +10,17 @@ package extension
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(
name: String,
value: String,
value: String?,
) {
buildConfigField(
type = "String",
name = name,
value = "\"$value\""
value = value?.let { "\"$it\"" } ?: "null"
)
}