diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/SimpleLocationTrackingEffect.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/SimpleLocationTrackingEffect.kt new file mode 100644 index 0000000000..7a7a1551cf --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/SimpleLocationTrackingEffect.kt @@ -0,0 +1,52 @@ +/* + * 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.impl.common.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import org.maplibre.compose.location.Location +import org.maplibre.compose.location.UserLocationState +import kotlin.math.abs + +/** + * Drop-in replacement for the library's LocationTrackingEffect. + * TODO remove once https://github.com/maplibre/maplibre-compose/issues/808 is fixed + */ +@Composable +internal fun SimpleLocationTrackingEffect( + locationState: UserLocationState, + enabled: Boolean = true, + precision: Double = 0.00001, + onLocationChange: suspend (Location?) -> Unit, +) { + val latestOnLocationChange by rememberUpdatedState(onLocationChange) + + LaunchedEffect(locationState, enabled) { + if (!enabled) return@LaunchedEffect + val locationStateFlow = snapshotFlow { locationState.location } + locationStateFlow + .distinctUntilChanged { oldLocation, newLocation -> + if (oldLocation != null && newLocation != null) { + when { + abs(oldLocation.position.value.latitude - newLocation.position.value.latitude) >= precision -> false + abs(oldLocation.position.value.longitude - newLocation.position.value.longitude) >= precision -> false + else -> true + } + } else { + false + } + } + .collect { location -> + latestOnLocationChange(location) + } + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt index 589ed87c6f..7bad736911 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/UserLocationPuck.kt @@ -18,11 +18,11 @@ import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.LocationPuck import org.maplibre.compose.location.LocationPuckColors import org.maplibre.compose.location.LocationPuckSizes -import org.maplibre.compose.location.LocationTrackingEffect import org.maplibre.compose.location.UserLocationState import org.maplibre.compose.location.rememberAndroidLocationProvider import org.maplibre.compose.location.rememberNullLocationProvider import org.maplibre.compose.location.rememberUserLocationState +import org.maplibre.spatialk.units.extensions.meters import kotlin.time.Duration.Companion.seconds @Composable @@ -31,22 +31,24 @@ fun UserLocationPuck( locationState: UserLocationState, trackUserLocation: Boolean, ) { - LocationTrackingEffect( + SimpleLocationTrackingEffect( locationState = locationState, enabled = trackUserLocation, - ) { - val finalPosition = cameraState.position.copy( - target = currentLocation.position, - bearing = currentLocation.bearing ?: cameraState.position.bearing, + ) { currentLocation -> + val target = currentLocation?.position?.value ?: cameraState.position.target + val newPosition = cameraState.position.copy( + target = target, + // Force pointing to NORTH + bearing = 0.0, zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM) ) - cameraState.animateTo(finalPosition) + cameraState.animateTo(newPosition) } val location = locationState.location if (location != null) { LocationPuck( idPrefix = "user-location", - locationState = locationState, + location = location, cameraState = cameraState, accuracyThreshold = Float.POSITIVE_INFINITY, showBearingAccuracy = false, @@ -74,7 +76,7 @@ fun rememberUserLocationState(hasLocationPermission: Boolean): UserLocationState rememberAndroidLocationProvider( updateInterval = 5.seconds, desiredAccuracy = DesiredAccuracy.High, - minDistanceMeters = 5f, + minDistance = 5.meters, ) } return rememberUserLocationState(locationProvider) diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt index 4451febb19..a926f1255b 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/live/service/LiveLocationSharingService.kt @@ -36,6 +36,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import org.maplibre.compose.location.AndroidLocationProvider import org.maplibre.compose.location.DesiredAccuracy +import org.maplibre.spatialk.units.extensions.inMeters +import org.maplibre.spatialk.units.extensions.meters import timber.log.Timber import kotlin.time.Duration.Companion.seconds import io.element.android.features.location.api.Location as ApiLocation @@ -91,7 +93,7 @@ class LiveLocationSharingService : Service() { val locationProvider = AndroidLocationProvider( context = applicationContext, updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds, - minDistanceMeters = minDistanceMeters.toFloat(), + minDistance = minDistanceMeters.meters, desiredAccuracy = DesiredAccuracy.Balanced, coroutineScope = coroutineScope ) @@ -100,9 +102,9 @@ class LiveLocationSharingService : Service() { .filterNotNull() .map { location -> ApiLocation( - lat = location.position.latitude, - lon = location.position.longitude, - accuracy = location.accuracy.toFloat(), + lat = location.position.value.latitude, + lon = location.position.value.longitude, + accuracy = location.position.accuracy?.inMeters?.toFloat(), ) } .onEach(coordinator::dispatch) 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 9f4c8333b8..6a769f8958 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 @@ -100,8 +100,8 @@ class ShareLocationPresenter( fun checkLocationConstraints() { // No need to check SendLiveLocationPermissions here val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED) - dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState()) trackUserPosition = locationConstraints is LocationConstraintsCheck.Success + dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState()) } suspend fun computeLiveLocationDialogState(): ShareLocationState.Dialog { 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 52400cd49d..68dcee36ac 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 @@ -225,8 +225,8 @@ private fun BottomSheetContent( state.eventSink( ShareLocationEvent.ShareStaticLocation( location = Location( - lat = userLocation.position.latitude, - lon = userLocation.position.longitude + lat = userLocation.position.value.latitude, + lon = userLocation.position.value.longitude ), isPinned = false ) 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 231bc01b12..5d5295871c 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 @@ -155,7 +155,9 @@ fun ShowLocationView( val position = CameraPosition( padding = sheetPaddings, target = Position(locationShare.location.lon, locationShare.location.lat), - zoom = MapDefaults.DEFAULT_ZOOM + // Force pointing to NORTH + bearing = 0.0, + zoom = cameraState.position.zoom.coerceAtLeast(MapDefaults.DEFAULT_ZOOM), ) coroutineScope.launch { cameraState.animateTo(finalPosition = position) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f3e79e3c45..05530df227 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -214,7 +214,7 @@ telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = " statemachine = "com.freeletics.flowredux:compose:1.2.2" maplibre = "org.maplibre.gl:android-sdk:13.1.0" maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" -maplibre_compose = "org.maplibre.compose:maplibre-compose:0.12.1" +maplibre_compose = "org.maplibre.compose:maplibre-compose:0.13.0" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2" opusencoder = "io.element.android:opusencoder:1.2.0" zxing_cpp = "io.github.zxing-cpp:android:3.0.2"