Disable location sharing when no MapTiler config is available

LocationService now exposes the MapTiler config to use: the one provided by the
homeserver takes precedence over the build time one, and null means that no map
provider is configured. It is now session scoped, and isServiceAvailable()
returns a StateFlow derived from this config.

When no config is available:
- the location sharing option is hidden from the composer, and location events
  cannot be opened in the location viewer;
- the "Location sharing" entry is hidden from the settings;
- location events in the timeline render a placeholder, without performing any
  request to MapTiler.

The show and share location screens now get the config from LocationService
rather than fetching the homeserver config by themselves.

Closes element-hq/element-android-enterprise#69

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benoit Marty
2026-10-13 17:15:37 +02:00
parent 5104e044ff
commit 4b97d6ab96
28 changed files with 212 additions and 106 deletions
@@ -75,6 +75,7 @@ dependencies {
implementation(projects.features.analytics.api)
implementation(projects.features.enterprise.api)
implementation(projects.features.licenses.api)
implementation(projects.features.location.api)
implementation(projects.features.logout.api)
implementation(projects.features.deactivation.api)
implementation(projects.features.invite.api)
@@ -109,6 +110,7 @@ dependencies {
testImplementation(projects.features.enterprise.test)
testImplementation(projects.features.invite.test)
testImplementation(projects.features.licenses.test)
testImplementation(projects.features.location.test)
testImplementation(projects.features.lockscreen.test)
testImplementation(projects.features.logout.test)
testImplementation(projects.libraries.indicator.test)
@@ -19,6 +19,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.mapToTheme
import io.element.android.features.location.api.LocationService
import io.element.android.features.preferences.impl.account.PreferencesAccountState
import io.element.android.features.preferences.impl.userstatus.UserStatusState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
@@ -47,6 +48,7 @@ class PreferencesRootPresenter(
private val snackbarDispatcher: SnackbarDispatcher,
private val showDeveloperSettingsProvider: ShowDeveloperSettingsProvider,
private val featureFlagService: FeatureFlagService,
private val locationService: LocationService,
private val sessionStore: SessionStore,
private val appPreferencesStore: AppPreferencesStore,
private val userStatusPresenter: Presenter<UserStatusState>,
@@ -115,6 +117,8 @@ class PreferencesRootPresenter(
val showDeveloperSettings by showDeveloperSettingsProvider.showDeveloperSettings.collectAsState()
val isLocationServiceAvailable by remember { locationService.isServiceAvailable() }.collectAsState()
fun handleEvent(event: PreferencesRootEvent) {
when (event) {
is PreferencesRootEvent.OnVersionInfoClick -> {
@@ -150,6 +154,7 @@ class PreferencesRootPresenter(
showAnalyticsSettings = hasAnalyticsProviders,
showDeveloperSettings = showDeveloperSettings,
showLabsItem = showLabsItem,
showLocationSettings = isLocationServiceAvailable,
snackbarMessage = snackbarMessage,
eventSink = ::handleEvent,
)
@@ -32,6 +32,7 @@ data class PreferencesRootState(
val showAnalyticsSettings: Boolean,
val showDeveloperSettings: Boolean,
val showLabsItem: Boolean,
val showLocationSettings: Boolean,
val snackbarMessage: SnackbarMessage?,
val eventSink: (PreferencesRootEvent) -> Unit,
)
@@ -53,6 +53,8 @@ open class PreferencesRootStatePreviewParam : PreviewParameterProvider<Preferenc
aPreferencesRootState(userStatusState = aUserStatusState(pickerState = UserStatusPickerState.ShowingPicker)),
// Minimal state
aPreferencesRootState(userStatusState = null),
// Location sharing not available
aPreferencesRootState(showLocationSettings = false),
)
}
@@ -68,6 +70,7 @@ fun aPreferencesRootState(
showAnalyticsSettings: Boolean = false,
showDeveloperSettings: Boolean = false,
showLabsItem: Boolean = false,
showLocationSettings: Boolean = true,
userStatusState: UserStatusState? = aUserStatusState(),
snackbarMessage: SnackbarMessage? = null,
eventSink: (PreferencesRootEvent) -> Unit = {},
@@ -83,6 +86,7 @@ fun aPreferencesRootState(
showAnalyticsSettings = showAnalyticsSettings,
showDeveloperSettings = showDeveloperSettings,
showLabsItem = showLabsItem,
showLocationSettings = showLocationSettings,
userStatusState = userStatusState,
snackbarMessage = snackbarMessage,
eventSink = eventSink,
@@ -330,11 +330,13 @@ private fun AppSettingsSection(
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Lock())),
onClick = onOpenLockScreenSettings,
)
ListItem(
content = { Text(stringResource(id = CommonStrings.common_location_sharing)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.LocationPin())),
onClick = onOpenLocationSettings,
)
if (state.showLocationSettings) {
ListItem(
content = { Text(stringResource(id = CommonStrings.common_location_sharing)) },
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.LocationPin())),
onClick = onOpenLocationSettings,
)
}
}
}
@@ -13,6 +13,8 @@ package io.element.android.features.preferences.impl.root
import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat
import io.element.android.compound.theme.Theme
import io.element.android.features.location.api.LocationService
import io.element.android.features.location.test.FakeLocationService
import io.element.android.features.preferences.impl.account.aPreferencesAccountState
import io.element.android.features.preferences.impl.userstatus.aUserStatusState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
@@ -78,10 +80,21 @@ class PreferencesRootPresenterTest {
assertThat(loadedState.showAnalyticsSettings).isFalse()
assertThat(loadedState.showDeveloperSettings).isTrue()
assertThat(loadedState.showLabsItem).isFalse()
assertThat(loadedState.showLocationSettings).isTrue()
assertThat(loadedState.snackbarMessage).isNull()
}
}
@Test
fun `present - location settings are hidden when the location service is not available`() = runTest {
createPreferencesRootPresenter(
locationService = FakeLocationService(isServiceAvailable = false),
).test {
assertThat(awaitItem().showLocationSettings).isFalse()
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - user status state is null when the server does not support it`() = runTest {
createPreferencesRootPresenter(
@@ -291,6 +304,7 @@ class PreferencesRootPresenterTest {
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
sessionStore: SessionStore = InMemorySessionStore(),
appPreferencesStore: AppPreferencesStore = InMemoryAppPreferencesStore(),
locationService: LocationService = FakeLocationService(isServiceAvailable = true),
) = PreferencesRootPresenter(
matrixClient = matrixClient,
analyticsService = FakeAnalyticsService(),
@@ -298,6 +312,7 @@ class PreferencesRootPresenterTest {
snackbarDispatcher = SnackbarDispatcher(),
showDeveloperSettingsProvider = showDeveloperSettingsProvider,
featureFlagService = featureFlagService,
locationService = locationService,
sessionStore = sessionStore,
appPreferencesStore = appPreferencesStore,
userStatusPresenter = { aUserStatusState() },