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 fed49bdeab..188346f12a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -14,7 +14,6 @@ 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 @@ -52,13 +51,13 @@ import io.element.android.features.callnative.api.NativeCallHost 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.CustomMapTilerConfigProvider 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.LocationService 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,7 +152,7 @@ class LoggedInFlowNode( private val analyticsRoomListStateWatcher: AnalyticsRoomListStateWatcher, private val createRoomEntryPoint: CreateRoomEntryPoint, private val activeLiveLocationShareManager: ActiveLiveLocationShareManager, - private val customMapTilerConfigProvider: CustomMapTilerConfigProvider, + private val locationService: LocationService, private val loggedInEventProcessorFactory: LoggedInEventProcessor.Factory, ) : BaseFlowNode( backstack = BackStack( @@ -636,10 +635,7 @@ class LoggedInFlowNode( enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId) }.collectAsState(SemanticColorsLightDark.default) - val currentMapTilerConfig = LocalMapTilerConfig.current - val updatedMapTilerConfig by produceState(currentMapTilerConfig) { - value = customMapTilerConfigProvider.get().getOrNull() ?: currentMapTilerConfig - } + val mapTilerConfig by locationService.mapTilerConfig.collectAsState() ElementThemeApp( appPreferencesStore = appPreferencesStore, @@ -648,7 +644,7 @@ class LoggedInFlowNode( compoundDark = colors.dark, buildMeta = buildMeta, ) { - CompositionLocalProvider(LocalMapTilerConfig provides updatedMapTilerConfig) { + CompositionLocalProvider(LocalMapTilerConfig provides mapTilerConfig) { val isOnline by syncService.isOnline.collectAsState() // Outside the connectivity indicator so the two strips stack rather than fight: an // offline banner during a call belongs under the call bar, not over it. 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 index 752dd2401c..00f9d5190c 100644 --- 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 @@ -11,13 +11,22 @@ import androidx.compose.runtime.staticCompositionLocalOf import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig /** - * Provides a [MapTilerConfig] for the MapLibre maps. + * Provides the [MapTilerConfig] for the MapLibre maps, or null if no map provider is configured. */ -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 - ) +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 + ) + } } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt index 41c58c396f..02cb7bba14 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/LocationService.kt @@ -8,10 +8,21 @@ package io.element.android.features.location.api +import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig +import io.element.android.libraries.core.coroutine.mapState +import kotlinx.coroutines.flow.StateFlow + /** - * Tells whether the location features can be used at all, which depends on the build having a map provider configured. + * Tells whether the location features can be used at all, which depends on a map provider being configured, + * either by the homeserver or at build time. */ interface LocationService { + /** + * The [MapTilerConfig] to use to render the maps, or null if no map provider is configured. + * The configuration provided by the homeserver takes precedence over the one defined at build time. + */ + val mapTilerConfig: StateFlow + /** Whether location sharing and map rendering are available; `false` disables the whole feature in the UI. */ - fun isServiceAvailable(): Boolean + fun isServiceAvailable(): StateFlow = mapTilerConfig.mapState { it != null } } 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 b3149964e4..f6a70f6653 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 @@ -50,11 +50,12 @@ import io.element.android.libraries.designsystem.utils.CommonDrawables /** * Shows a static map image downloaded via a third party service's static maps API. * - * Handles 4 distinct cases: + * Handles 5 distinct cases: * 1. Stale location (pinVariant is StaleLocation) - shows stale map with stale pin, no fetching * 2. Null location - shows blurred placeholder, no pin, no loading - * 3. Loading (location != null, fetching) - shows blurred placeholder with loading indicator - * 4. Success (location != null, loaded) - shows actual map with pin + * 3. Null mapTilerConfig (no map provider configured) - shows blurred placeholder, no pin, no fetching + * 4. Loading (location != null, fetching) - shows blurred placeholder with loading indicator + * 5. Success (location != null, loaded) - shows actual map with pin */ @Composable fun StaticMapView( @@ -84,7 +85,8 @@ fun StaticMapView( ) } // Case 2: Null location - show blurred placeholder, no pin, no loading - location == null -> { + // Case 3: No map provider configured - show blurred placeholder, no pin, no fetching + location == null || mapTilerConfig == null && !LocalInspectionMode.current -> { StaticMapPlaceholder( painter = painterResource(R.drawable.blurred_map), canReload = false, @@ -94,7 +96,7 @@ fun StaticMapView( onLoadMapClick = {} ) } - // Cases 3 & 4: Non-null location - fetch map + // Cases 4 & 5: Non-null location - fetch map else -> LoadableMapContent( mapTilerConfig = mapTilerConfig, location = location, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt index 9f2c3fa70b..aa14e6505b 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/DefaultLocationService.kt @@ -8,14 +8,31 @@ package io.element.android.features.location.impl -import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding -import io.element.android.features.location.api.BuildConfig +import dev.zacsweers.metro.SingleIn +import io.element.android.features.enterprise.api.remoteconfig.CustomMapTilerConfigProvider +import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig import io.element.android.features.location.api.LocationService +import io.element.android.features.location.api.buildTimeMapTilerConfig +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.di.annotations.SessionCoroutineScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn -@ContributesBinding(AppScope::class) -class DefaultLocationService : LocationService { - override fun isServiceAvailable(): Boolean { - return BuildConfig.MAPTILER_API_KEY.isNotEmpty() - } +@SingleIn(SessionScope::class) +@ContributesBinding(SessionScope::class) +class DefaultLocationService( + customMapTilerConfigProvider: CustomMapTilerConfigProvider, + @SessionCoroutineScope sessionCoroutineScope: CoroutineScope, +) : LocationService { + override val mapTilerConfig: StateFlow = flow { + customMapTilerConfigProvider.get().getOrNull()?.let { emit(it) } + }.stateIn( + scope = sessionCoroutineScope, + started = SharingStarted.Eagerly, + initialValue = buildTimeMapTilerConfig(), + ) } 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 05af96fe5e..4dbc2fac54 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 @@ -14,7 +14,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -22,7 +21,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.CustomMapTilerConfigProvider +import io.element.android.features.location.api.LocationService 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 @@ -38,7 +37,6 @@ import io.element.android.features.location.impl.common.userlocation.UserLocatio import io.element.android.features.location.impl.live.LiveLocationStore import io.element.android.features.messages.api.MessageComposerContext import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runUpdatingState import io.element.android.libraries.core.extensions.flatMap @@ -73,7 +71,7 @@ class ShareLocationPresenter( private val liveLocationShareManager: ActiveLiveLocationShareManager, private val liveLocationStore: LiveLocationStore, private val userLocationStateFactory: UserLocationState.Factory, - private val customMapTilerConfigProvider: CustomMapTilerConfigProvider, + private val locationService: LocationService, ) : Presenter { @AssistedFactory fun interface Factory { @@ -94,10 +92,7 @@ class ShareLocationPresenter( var pendingLiveLocationShare by remember { mutableStateOf(false) } val startLiveLocationAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } val currentUser by client.userProfile.collectAsState() - val customMapConfig by produceState(AsyncData.Loading()) { - // Ignore errors - value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull()) - } + val mapTilerConfig by locationService.mapTilerConfig.collectAsState() val sendLiveLocationPermissions by room.permissionsAsState(SendLiveLocationPermissions.DEFAULT) { perms -> perms.sendLiveLocationPermissions() } @@ -201,7 +196,7 @@ class ShareLocationPresenter( } return ShareLocationState( - customMapTilerConfig = customMapConfig, + mapTilerConfig = mapTilerConfig, 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 b29228f866..38747b866f 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 @@ -12,12 +12,11 @@ import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.userlocation.UserLocationState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList data class ShareLocationState( - val customMapTilerConfig: AsyncData, + val mapTilerConfig: MapTilerConfig?, 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/ShareLocationStatePreviewParam.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStatePreviewParam.kt index 908c7f0641..7a192fc4a8 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStatePreviewParam.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStatePreviewParam.kt @@ -12,8 +12,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.userlocation.UserLocationState +import io.element.android.features.location.impl.show.aPreviewMapTilerConfig 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 kotlinx.collections.immutable.persistentListOf @@ -71,13 +71,13 @@ class ShareLocationStatePreviewParam : PreviewParameterProvider = AsyncData.Success(null), + mapTilerConfig: MapTilerConfig? = aPreviewMapTilerConfig(), currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")), dialogState: ShareLocationState.Dialog = ShareLocationState.Dialog.None, trackUserPosition: Boolean = false, @@ -88,7 +88,7 @@ fun aShareLocationState( eventSink: (ShareLocationEvent) -> Unit = {}, ): ShareLocationState { return ShareLocationState( - customMapTilerConfig = customMapTilerConfig, + mapTilerConfig = mapTilerConfig, currentUser = currentUser, dialogState = dialogState, trackUserLocation = trackUserPosition, 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 6baa7bf3d2..d19d3fe0f0 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 @@ -130,7 +130,7 @@ private fun ShareLocationViewContent( navigateUp: () -> Unit, modifier: Modifier = Modifier, ) { - val tileStyleUrl = rememberTileStyleUrl(state.customMapTilerConfig.dataOrNull()) + val tileStyleUrl = rememberTileStyleUrl(state.mapTilerConfig) val mapState = rememberMapState( baseStyle = BaseStyle.Uri(tileStyleUrl), initialCameraPosition = MapDefaults.defaultCameraPosition, @@ -181,7 +181,7 @@ private fun ShareLocationScaffold( ) ) MapBottomSheetScaffold( - isReady = state.customMapTilerConfig.isReady(), + isReady = state.mapTilerConfig != null, mapState = mapState, 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 51ed1d6c68..0a9911c350 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,8 +22,8 @@ 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.CustomMapTilerConfigProvider import io.element.android.features.location.api.Location +import io.element.android.features.location.api.LocationService import io.element.android.features.location.api.ShowLocationMode import io.element.android.features.location.api.live.ActiveLiveLocationShareManager import io.element.android.features.location.api.live.isCurrentlySharing @@ -39,7 +39,6 @@ import io.element.android.features.location.impl.common.toDialogState import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.userlocation.UserLocationState import io.element.android.features.location.impl.common.userlocation.toLocationMeasurement -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.meta.BuildMeta @@ -68,7 +67,7 @@ class ShowLocationPresenter( private val joinedRoom: JoinedRoom, private val liveLocationShareManager: ActiveLiveLocationShareManager, private val userLocationStateFactory: UserLocationState.Factory, - private val customMapTilerConfigProvider: CustomMapTilerConfigProvider, + private val locationService: LocationService, ) : Presenter { @AssistedFactory fun interface Factory { fun create(mode: ShowLocationMode): ShowLocationPresenter @@ -86,10 +85,7 @@ class ShowLocationPresenter( mutableStateOf(LocationConstraintsDialogState.None) } - val customMapTilerConfig by produceState(AsyncData.Loading()) { - // Ignore errors - value = AsyncData.Success(customMapTilerConfigProvider.get().getOrNull()) - } + val mapTilerConfig by locationService.mapTilerConfig.collectAsState() fun checkLocationConstraints() { val locationConstraints = checkLocationConstraints( @@ -221,7 +217,7 @@ class ShowLocationPresenter( userLocationStateFactory.create(hasLocationPermission = permissionsState.isAnyGranted) } return ShowLocationState( - customMapTilerConfig = customMapTilerConfig, + mapTilerConfig = mapTilerConfig, 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 0472eb94e9..79dacdad1f 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 @@ -13,7 +13,6 @@ import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.ui.LocationMarkerData import io.element.android.features.location.impl.common.userlocation.UserLocationState -import io.element.android.libraries.architecture.AsyncData 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 @@ -21,7 +20,7 @@ import io.element.android.libraries.matrix.api.room.location.AssetType import kotlinx.collections.immutable.ImmutableList data class ShowLocationState( - val customMapTilerConfig: AsyncData, + val mapTilerConfig: MapTilerConfig?, 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/ShowLocationStatePreviewParam.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStatePreviewParam.kt index 8473eaeb04..4c106c76a5 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStatePreviewParam.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStatePreviewParam.kt @@ -13,7 +13,6 @@ import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig import io.element.android.features.location.api.Location import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.common.userlocation.UserLocationState -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE @@ -37,14 +36,21 @@ class ShowLocationStatePreviewParam : PreviewParameterProvider = AsyncData.Success(null), + mapTilerConfig: MapTilerConfig? = aPreviewMapTilerConfig(), isLive: Boolean = false, constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None, locationShares: List = listOf(aLocationShareItem(isLive = isLive)), @@ -56,7 +62,7 @@ fun aShowLocationState( eventSink: (ShowLocationEvent) -> Unit = {}, ): ShowLocationState { return ShowLocationState( - customMapTilerConfig = customMapTilerConfig, + mapTilerConfig = mapTilerConfig, 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 8a797ad1a6..483310a78a 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 @@ private fun ShowLocationViewContent( onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { - val tileStyleUrl = rememberTileStyleUrl(state.customMapTilerConfig.dataOrNull()) + val tileStyleUrl = rememberTileStyleUrl(state.mapTilerConfig) val mapState = rememberMapState( baseStyle = BaseStyle.Uri(tileStyleUrl), initialCameraPosition = MapDefaults.defaultCameraPosition, @@ -178,7 +178,7 @@ private fun ShowLocationScaffold( } } MapBottomSheetScaffold( - isReady = state.customMapTilerConfig.isReady(), + isReady = state.mapTilerConfig != null, sheetDragHandle = if (state.isSheetDraggable) { { BottomSheetDefaults.DragHandle() } } else { diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt index b0e899730b..00c7fc89b5 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/DefaultLocationServiceTest.kt @@ -9,15 +9,56 @@ 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 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Test +@OptIn(ExperimentalCoroutinesApi::class) class DefaultLocationServiceTest { @Test - fun `isServiceAvailable should return value depending on BuildConfig MAPTILER_API_KEY`() { - val locationService = DefaultLocationService() - assertThat(locationService.isServiceAvailable()).isEqualTo( - BuildConfig.MAPTILER_API_KEY.isNotEmpty() + fun `when the homeserver does not provide a config, the build time config is used`() = runTest { + val locationService = createDefaultLocationService( + customMapTilerConfigProvider = { Result.success(null) }, ) + runCurrent() + assertThat(locationService.mapTilerConfig.value).isEqualTo(buildTimeMapTilerConfig()) + assertThat(locationService.isServiceAvailable().value).isEqualTo(BuildConfig.MAPTILER_API_KEY.isNotEmpty()) } + + @Test + fun `when the homeserver config cannot be retrieved, the build time config is used`() = runTest { + val locationService = createDefaultLocationService( + customMapTilerConfigProvider = { Result.failure(AN_EXCEPTION) }, + ) + runCurrent() + assertThat(locationService.mapTilerConfig.value).isEqualTo(buildTimeMapTilerConfig()) + assertThat(locationService.isServiceAvailable().value).isEqualTo(BuildConfig.MAPTILER_API_KEY.isNotEmpty()) + } + + @Test + fun `when the homeserver provides a config, it is used and the service is available`() = runTest { + val config = aMapTilerConfig(apiKey = "homeserver_api_key") + val locationService = createDefaultLocationService( + customMapTilerConfigProvider = { Result.success(config) }, + ) + runCurrent() + assertThat(locationService.mapTilerConfig.value).isEqualTo(config) + assertThat(locationService.isServiceAvailable().value).isTrue() + } + + private fun TestScope.createDefaultLocationService( + customMapTilerConfigProvider: CustomMapTilerConfigProvider, + sessionCoroutineScope: CoroutineScope = backgroundScope, + ) = DefaultLocationService( + customMapTilerConfigProvider = customMapTilerConfigProvider, + sessionCoroutineScope = sessionCoroutineScope, + ) } 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 a37011e1d9..e57fafa61b 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 @@ -16,6 +16,7 @@ import io.element.android.features.location.impl.common.actions.FakeLocationActi import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter import io.element.android.features.location.impl.live.LiveLocationStore import io.element.android.features.location.test.FakeActiveLiveLocationShareManager +import io.element.android.features.location.test.FakeLocationService import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.dateformatter.test.FakeDurationFormatter import io.element.android.libraries.matrix.api.timeline.Timeline @@ -58,7 +59,7 @@ class DefaultShareLocationEntryPointTest { sessionId = room.sessionId, ), userLocationStateFactory = FakeUserLocationStateFactory(), - customMapTilerConfigProvider = { Result.success(null) }, + locationService = FakeLocationService(), ) }, 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 129ea1e4cd..3dcb358cf1 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,9 +19,9 @@ 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.MapTilerConfig import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig import io.element.android.features.location.api.Location +import io.element.android.features.location.api.LocationService import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.common.FakeUserLocationStateFactory import io.element.android.features.location.impl.common.actions.FakeLocationActions @@ -31,6 +31,7 @@ import io.element.android.features.location.impl.common.permissions.PermissionsS import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState import io.element.android.features.location.impl.live.LiveLocationStore import io.element.android.features.location.test.FakeActiveLiveLocationShareManager +import io.element.android.features.location.test.FakeLocationService import io.element.android.features.messages.test.FakeMessageComposerContext import io.element.android.libraries.dateformatter.test.FakeDurationFormatter import io.element.android.libraries.matrix.api.core.EventId @@ -89,7 +90,7 @@ class ShareLocationPresenterTest { liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), liveLocationStore: LiveLocationStore = createLiveLocationStore(sessionId = joinedRoom.sessionId), client: FakeMatrixClient = fakeMatrixClient, - customMapTilerConfigProvider: () -> Result = { Result.success(null) }, + locationService: LocationService = FakeLocationService(mapTilerConfig = aMapTilerConfig()), ): ShareLocationPresenter = ShareLocationPresenter( permissionsPresenterFactory = { fakePermissionsPresenter }, room = joinedRoom, @@ -103,7 +104,7 @@ class ShareLocationPresenterTest { liveLocationShareManager = liveLocationShareManager, liveLocationStore = liveLocationStore, userLocationStateFactory = FakeUserLocationStateFactory(), - customMapTilerConfigProvider = customMapTilerConfigProvider, + locationService = locationService, ) @Test @@ -118,26 +119,24 @@ class ShareLocationPresenterTest { val shareLocationPresenter = createShareLocationPresenter() shareLocationPresenter.test { val state = awaitFirstItem() - assertThat(state.customMapTilerConfig.isLoading()).isFalse() + assertThat(state.mapTilerConfig).isNotNull() assertThat(state.trackUserLocation).isTrue() assertThat(state.dialogState).isEqualTo(ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.None)) } } @Test - fun `present - non-null customMapStyleUrl`() = runTest { + fun `present - mapTilerConfig is provided by the location service`() = runTest { val mapTilerConfig = aMapTilerConfig(apiKey = "A KEY") val shareLocationPresenter = createShareLocationPresenter( client = FakeMatrixClient( sessionId = A_USER_ID, ), - customMapTilerConfigProvider = { Result.success(mapTilerConfig) } + locationService = FakeLocationService(mapTilerConfig = mapTilerConfig), ) shareLocationPresenter.test { - val state = awaitItem() - assertThat(state.customMapTilerConfig.isLoading()).isTrue() - val finalState = awaitItem() - assertThat(finalState.customMapTilerConfig.dataOrNull()).isEqualTo(mapTilerConfig) + assertThat(awaitItem().mapTilerConfig).isEqualTo(mapTilerConfig) + cancelAndIgnoreRemainingEvents() } } 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 db5c6b9f9f..83172d1079 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 @@ -18,6 +18,7 @@ import io.element.android.features.location.impl.common.FakeUserLocationStateFac import io.element.android.features.location.impl.common.actions.FakeLocationActions import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter import io.element.android.features.location.test.FakeActiveLiveLocationShareManager +import io.element.android.features.location.test.FakeLocationService import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.core.aBuildMeta @@ -51,7 +52,7 @@ class DefaultShowLocationEntryPointTest { joinedRoom = joinedRoom, liveLocationShareManager = FakeActiveLiveLocationShareManager(), userLocationStateFactory = FakeUserLocationStateFactory(), - customMapTilerConfigProvider = { Result.success(null) }, + locationService = FakeLocationService(), ) }, 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 b2a84d24e1..ab1a1af734 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,9 +12,9 @@ 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.api.remoteconfig.CustomMapTilerConfigProvider import io.element.android.features.enterprise.test.remoteconfig.aMapTilerConfig import io.element.android.features.location.api.Location +import io.element.android.features.location.api.LocationService import io.element.android.features.location.api.ShowLocationMode import io.element.android.features.location.impl.aPermissionsState import io.element.android.features.location.impl.common.FakeUserLocationStateFactory @@ -24,6 +24,7 @@ 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.location.test.FakeLocationService import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.JoinedRoom @@ -66,7 +67,7 @@ class ShowLocationPresenterTest { locationActions: FakeLocationActions = fakeLocationActions, joinedRoom: JoinedRoom = FakeJoinedRoom(), liveLocationShareManager: FakeActiveLiveLocationShareManager = FakeActiveLiveLocationShareManager(), - customMapTilerConfigProvider: CustomMapTilerConfigProvider = { Result.success(null) }, + locationService: LocationService = FakeLocationService(mapTilerConfig = aMapTilerConfig()), ) = ShowLocationPresenter( mode = mode, permissionsPresenterFactory = { fakePermissionsPresenter }, @@ -77,18 +78,16 @@ class ShowLocationPresenterTest { joinedRoom = joinedRoom, liveLocationShareManager = liveLocationShareManager, userLocationStateFactory = FakeUserLocationStateFactory(), - customMapTilerConfigProvider = customMapTilerConfigProvider, + locationService = locationService, ) @Test - fun `present - non-null customMapStyleUrl`() = runTest { + fun `present - mapTilerConfig is provided by the location service`() = runTest { val mapTilerConfig = aMapTilerConfig(apiKey = "A KEY") - val shareLocationPresenter = createShowLocationPresenter(customMapTilerConfigProvider = { Result.success(mapTilerConfig) }) - shareLocationPresenter.test { - val state = awaitItem() - assertThat(state.customMapTilerConfig.isLoading()).isTrue() - val finalState = awaitItem() - assertThat(finalState.customMapTilerConfig.dataOrNull()).isEqualTo(mapTilerConfig) + val showLocationPresenter = createShowLocationPresenter(locationService = FakeLocationService(mapTilerConfig = mapTilerConfig)) + showLocationPresenter.test { + assertThat(awaitItem().mapTilerConfig).isEqualTo(mapTilerConfig) + cancelAndIgnoreRemainingEvents() } } @@ -120,7 +119,7 @@ class ShowLocationPresenterTest { fun `centers on user location`() = runTest { fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted)) - val presenter = createShowLocationPresenter(customMapTilerConfigProvider = { Result.success(aMapTilerConfig()) }) + val presenter = createShowLocationPresenter() presenter.test { val initialState = awaitItem() @@ -407,9 +406,6 @@ 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) diff --git a/features/location/test/build.gradle.kts b/features/location/test/build.gradle.kts index e51737d40c..e08b702f46 100644 --- a/features/location/test/build.gradle.kts +++ b/features/location/test/build.gradle.kts @@ -16,6 +16,7 @@ android { dependencies { api(projects.features.location.api) + implementation(projects.features.enterprise.api) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.tests.testutils) diff --git a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt index f961a81c9b..0ff1545131 100644 --- a/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt +++ b/features/location/test/src/main/kotlin/io/element/android/features/location/test/FakeLocationService.kt @@ -8,10 +8,21 @@ package io.element.android.features.location.test +import io.element.android.features.enterprise.api.remoteconfig.MapTilerConfig import io.element.android.features.location.api.LocationService +import kotlinx.coroutines.flow.MutableStateFlow class FakeLocationService( - private val isServiceAvailable: Boolean = false, + mapTilerConfig: MapTilerConfig? = null, ) : LocationService { - override fun isServiceAvailable() = isServiceAvailable + constructor(isServiceAvailable: Boolean) : this( + MapTilerConfig( + apiKey = "test_api_key", + lightStyleId = null, + darkStyleId = null, + baseUrl = null, + ).takeIf { isServiceAvailable } + ) + + override val mapTilerConfig = MutableStateFlow(mapTilerConfig) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 503d30c6d9..c81691b004 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -776,7 +776,7 @@ class MessagesFlowNode( assetType = event.content.assetType, ) } - NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable() } + NavTarget.LocationViewer(mode = mode).takeIf { locationService.isServiceAvailable().value } } else { displayVulkanNotSupportedError = true null diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 2974b99696..f0879d962e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -184,10 +184,7 @@ class MessageComposerPresenter( val cameraPermissionState = cameraPermissionPresenter.present() - val canShareLocation = remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - canShareLocation.value = locationService.isServiceAvailable() - } + val canShareLocation by remember { locationService.isServiceAvailable() }.collectAsState() val isSendGalleryMessagesEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.SendGalleryMessages) .collectAsState(initial = false) @@ -431,7 +428,7 @@ class MessageComposerPresenter( isInThreadTimeline = isInThread, showAttachmentSourcePicker = showAttachmentSourcePicker, showTextFormatting = showTextFormatting, - canShareLocation = canShareLocation.value, + canShareLocation = canShareLocation, suggestions = suggestions.toImmutableList(), resolveMentionDisplay = resolveMentionDisplay, resolveAtRoomMentionDisplay = resolveAtRoomMentionDisplay, diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index c137d6be06..277d78b50c 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -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) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt index 53234c3723..2bd55397c5 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenter.kt @@ -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, @@ -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, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt index 67b75a84a8..b51d83592f 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootState.kt @@ -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, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt index 57b7493dd5..1bbd9e0548 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootStatePreviewParam.kt @@ -53,6 +53,8 @@ open class PreferencesRootStatePreviewParam : PreviewParameterProvider Unit = {}, @@ -83,6 +86,7 @@ fun aPreferencesRootState( showAnalyticsSettings = showAnalyticsSettings, showDeveloperSettings = showDeveloperSettings, showLabsItem = showLabsItem, + showLocationSettings = showLocationSettings, userStatusState = userStatusState, snackbarMessage = snackbarMessage, eventSink = eventSink, diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt index 1449ece700..38c02f04e1 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootView.kt @@ -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, + ) + } } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt index 085b74e323..34f60cbae3 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/root/PreferencesRootPresenterTest.kt @@ -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() },