Merge pull request #7663 from element-hq/feature/fga/maplibre-compose-update

Update maplibre compose to 0.15
This commit is contained in:
ganfra
2026-09-08 17:15:32 +02:00
committed by GitHub
13 changed files with 284 additions and 193 deletions
+5
View File
@@ -288,6 +288,10 @@ dependencies {
implementation(projects.libraries.pushproviders.unifiedpush) implementation(projects.libraries.pushproviders.unifiedpush)
} }
// Google Play Services fused location backend. Discovered at runtime via ServiceLoader by the
// location feature.
"gplayImplementation"(libs.maplibre.compose.location.runtime.gms)
implementation(libs.appyx.core) implementation(libs.appyx.core)
implementation(libs.androidx.splash) implementation(libs.androidx.splash)
implementation(libs.androidx.core) implementation(libs.androidx.core)
@@ -325,6 +329,7 @@ licensee {
allow("BSD-3-Clause") allow("BSD-3-Clause")
allow("EPL-1.0") allow("EPL-1.0")
allowUrl("https://opensource.org/license/bsd-3-clause") allowUrl("https://opensource.org/license/bsd-3-clause")
allowUrl("https://opensource.org/license/bsd-2-clause")
allowUrl("https://opensource.org/licenses/MIT") allowUrl("https://opensource.org/licenses/MIT")
allowUrl("https://developer.android.com/studio/terms.html") allowUrl("https://developer.android.com/studio/terms.html")
allowUrl("https://www.zetetic.net/sqlcipher/license/") allowUrl("https://www.zetetic.net/sqlcipher/license/")
+1
View File
@@ -29,6 +29,7 @@ dependencies {
implementation(projects.features.enterprise.api) implementation(projects.features.enterprise.api)
implementation(projects.features.messages.api) implementation(projects.features.messages.api)
implementation(libs.maplibre.compose) implementation(libs.maplibre.compose)
runtimeOnly(libs.maplibre.compose.runtime.vulkan)
implementation(libs.coil) implementation(libs.coil)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)
@@ -9,11 +9,9 @@
package io.element.android.features.location.impl.common package io.element.android.features.location.impl.common
import android.Manifest import android.Manifest
import androidx.compose.ui.Alignment
import org.maplibre.compose.camera.CameraPosition import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.map.GestureOptions import org.maplibre.compose.map.GestureOptions
import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.OrnamentOptions
import org.maplibre.compose.map.RenderOptions import org.maplibre.compose.map.RenderOptions
import org.maplibre.spatialk.geojson.Position import org.maplibre.spatialk.geojson.Position
@@ -24,14 +22,6 @@ object MapDefaults {
val options = MapOptions( val options = MapOptions(
renderOptions = RenderOptions.Standard, renderOptions = RenderOptions.Standard,
gestureOptions = GestureOptions.Standard, gestureOptions = GestureOptions.Standard,
ornamentOptions = OrnamentOptions(
isLogoEnabled = true,
logoAlignment = Alignment.BottomStart,
isAttributionEnabled = true,
attributionAlignment = Alignment.BottomEnd,
isCompassEnabled = false,
isScaleBarEnabled = false,
)
) )
val defaultCameraPosition = CameraPosition( val defaultCameraPosition = CameraPosition(
@@ -7,16 +7,19 @@
package io.element.android.features.location.impl.common.ui package io.element.android.features.location.impl.common.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
@@ -32,7 +35,6 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.material3.rememberBottomSheetState import androidx.compose.material3.rememberBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -55,6 +57,9 @@ import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.map.MapOptions import org.maplibre.compose.map.MapOptions
import org.maplibre.compose.map.MaplibreMap import org.maplibre.compose.map.MaplibreMap
import org.maplibre.compose.overlay.ExpandingAttributionButton
import org.maplibre.compose.overlay.MapOverlay
import org.maplibre.compose.overlay.MaplibreLogo
import org.maplibre.compose.style.BaseStyle import org.maplibre.compose.style.BaseStyle
import org.maplibre.compose.util.MaplibreComposable import org.maplibre.compose.util.MaplibreComposable
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -115,10 +120,6 @@ fun MapBottomSheetScaffold(
PaddingValues(bottom = bottomPadding) PaddingValues(bottom = bottomPadding)
} }
} }
// Update camera position when sheet padding changes
LaunchedEffect(sheetPadding) {
cameraState.position = cameraState.position.copy(padding = sheetPadding)
}
BottomSheetScaffold( BottomSheetScaffold(
modifier = Modifier, modifier = Modifier,
sheetPeekHeight = sheetPeekHeight, sheetPeekHeight = sheetPeekHeight,
@@ -135,14 +136,27 @@ fun MapBottomSheetScaffold(
snackbarHost = snackbarHost, snackbarHost = snackbarHost,
topBar = topBar, topBar = topBar,
) { ) {
val ornamentOptions = mapOptions.ornamentOptions.copy(padding = sheetPadding)
val mapOptions = mapOptions.copy(ornamentOptions = ornamentOptions)
Box { Box {
when (customMapTilerConfig) { when (customMapTilerConfig) {
is AsyncData.Success -> { is AsyncData.Success -> {
MaplibreMap( MaplibreMap(
options = mapOptions, options = mapOptions,
cameraPadding = sheetPadding,
contentWindowInsets = WindowInsets(bottom = sheetPadding.calculateBottomPadding()),
baseStyle = BaseStyle.Uri(rememberTileStyleUrl(customMapTilerConfig.data)), baseStyle = BaseStyle.Uri(rememberTileStyleUrl(customMapTilerConfig.data)),
overlay = MapOverlay {
val style = styleState
Row(
Modifier
.align(Alignment.BottomStart)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
MaplibreLogo()
ExpandingAttributionButton(cameraState = cameraState, styleState = style)
}
},
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
cameraState = cameraState, cameraState = cameraState,
content = mapContent, content = mapContent,
@@ -7,14 +7,16 @@
package io.element.android.features.location.impl.common.userlocation package io.element.android.features.location.impl.common.userlocation
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.RoomScope
import org.maplibre.compose.location.DesiredAccuracy import kotlinx.coroutines.flow.mapNotNull
import org.maplibre.compose.location.rememberNullLocationProvider import org.maplibre.compose.location.LocationAccuracy
import org.maplibre.compose.location.LocationEvent
import org.maplibre.compose.location.LocationRequest
import org.maplibre.spatialk.units.extensions.meters import org.maplibre.spatialk.units.extensions.meters
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -22,17 +24,27 @@ import kotlin.time.Duration.Companion.seconds
class DefaultUserLocationStateFactory : UserLocationState.Factory { class DefaultUserLocationStateFactory : UserLocationState.Factory {
@Composable @Composable
override fun create(hasLocationPermission: Boolean): UserLocationState { override fun create(hasLocationPermission: Boolean): UserLocationState {
val locationProvider = if (!hasLocationPermission) { val locationProvider = if (hasLocationPermission) {
rememberNullLocationProvider() rememberLocationProvider()
} else { } else {
@SuppressLint("MissingPermission") rememberNotGrantedLocationProvider()
rememberPlatformLocationProvider(
updateInterval = 5.seconds,
desiredAccuracy = DesiredAccuracy.High,
minDistance = 5.meters,
)
} }
val location by locationProvider.location.collectAsState() val locationFlow = remember(locationProvider) {
val locationRequest = LocationRequest(
accuracy = LocationAccuracy.High,
minimumInterval = 5.seconds,
minimumDistance = 5.meters,
)
locationProvider
.updates(locationRequest)
.mapNotNull { event ->
when (event) {
is LocationEvent.Fix -> event.location
is LocationEvent.Unavailable -> null
}
}
}
val location by locationFlow.collectAsState(null)
return UserLocationState(location) return UserLocationState(location)
} }
} }
@@ -0,0 +1,191 @@
/*
* 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.userlocation
import android.Manifest
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationManager
import android.os.Handler
import android.os.HandlerThread
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
import androidx.core.os.ExecutorCompat
import io.element.android.libraries.core.data.tryOrNull
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOf
import org.maplibre.compose.location.AndroidLocationBackend
import org.maplibre.compose.location.LocationAccuracy
import org.maplibre.compose.location.LocationEvent
import org.maplibre.compose.location.LocationProvider
import org.maplibre.compose.location.LocationRequest
import org.maplibre.compose.location.LocationUnavailableReason
import org.maplibre.compose.location.asMapLibreLocation
import org.maplibre.spatialk.units.extensions.inMeters
import java.util.ServiceLoader
/**
* Creates the location provider to use.
*
* Mostly mirrors `createDefaultLocationProvider` from maplibre compose: any [AndroidLocationBackend]
* registered through [ServiceLoader] takes precedence, highest [priority]
* first. When no backend is available it falls back to [PlatformLocationProvider].
*/
fun createLocationProvider(context: Context): LocationProvider {
val backend = tryOrNull {
ServiceLoader.load(AndroidLocationBackend::class.java)
.filter { it.isAvailable(context) }
.minWithOrNull(compareByDescending<AndroidLocationBackend> { it.priority }.thenBy { it.id })
}
return backend?.createLocationProvider(context) ?: PlatformLocationProvider(context)
}
@Composable
fun rememberLocationProvider(context: Context = LocalContext.current): LocationProvider {
return remember(context) { createLocationProvider(context) }
}
class PlatformLocationProvider(
private val context: Context,
) : LocationProvider {
@SuppressLint("MissingPermission")
override fun updates(request: LocationRequest): Flow<LocationEvent> = callbackFlow {
if (!context.hasLocationPermission()) {
trySend(LocationEvent.Unavailable(LocationUnavailableReason.PermissionDenied))
close()
return@callbackFlow
}
val locationManager = context.getSystemService(LocationManager::class.java)
val listener = object : LocationListenerCompat {
override fun onLocationChanged(location: Location) {
trySend(LocationEvent.Fix(location.asMapLibreLocation()))
}
override fun onProviderDisabled(provider: String) {
trySend(LocationEvent.Unavailable(LocationUnavailableReason.ServicesDisabled))
}
}
var registered = false
fun refreshRegistration() {
if (registered) {
LocationManagerCompat.removeUpdates(locationManager, listener)
registered = false
}
if (!LocationManagerCompat.isLocationEnabled(locationManager)) {
trySend(LocationEvent.Unavailable(LocationUnavailableReason.ServicesDisabled))
return
}
val provider = PROVIDERS_BY_PRIORITY.firstOrNull { LocationManagerCompat.hasProvider(locationManager, it) }
if (provider == null) {
trySend(LocationEvent.Unavailable(LocationUnavailableReason.Unsupported))
return
}
locationManager.getLastKnownLocation(provider)?.let { location ->
trySend(LocationEvent.Fix(location.asMapLibreLocation()))
}
val locationRequestCompat = LocationRequestCompat.Builder(request.minimumInterval.inWholeMilliseconds)
.setQuality(request.accuracy.toLocationRequestQuality())
.setMinUpdateDistanceMeters(request.minimumDistance.inMeters.toFloat())
.build()
LocationManagerCompat.requestLocationUpdates(
locationManager,
provider,
locationRequestCompat,
ExecutorCompat.create(Handler(handlerThread.looper)),
listener,
)
registered = true
}
// Re-select and re-register when the user toggles location settings, so updates resume
// automatically once a usable provider comes back.
val settingsReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
try {
refreshRegistration()
} catch (error: SecurityException) {
trySend(LocationEvent.Unavailable(LocationUnavailableReason.PermissionDenied, error))
close()
} catch (error: IllegalArgumentException) {
trySend(LocationEvent.Unavailable(LocationUnavailableReason.UnexpectedFailure, error))
close()
}
}
}
try {
ContextCompat.registerReceiver(
context,
settingsReceiver,
IntentFilter().apply {
addAction(LocationManager.MODE_CHANGED_ACTION)
addAction(LocationManager.PROVIDERS_CHANGED_ACTION)
},
ContextCompat.RECEIVER_NOT_EXPORTED,
)
refreshRegistration()
} catch (error: SecurityException) {
trySend(LocationEvent.Unavailable(LocationUnavailableReason.PermissionDenied, error))
close()
} catch (error: IllegalArgumentException) {
trySend(LocationEvent.Unavailable(LocationUnavailableReason.UnexpectedFailure, error))
close()
}
awaitClose {
tryOrNull { context.unregisterReceiver(settingsReceiver) }
LocationManagerCompat.removeUpdates(locationManager, listener)
}
}
private companion object {
private val PROVIDERS_BY_PRIORITY = listOf(
LocationManager.FUSED_PROVIDER,
LocationManager.GPS_PROVIDER,
LocationManager.NETWORK_PROVIDER,
)
private val handlerThread by lazy {
HandlerThread("PlatformLocationProvider").apply { start() }
}
}
}
private fun Context.hasLocationPermission(): Boolean =
checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
private fun LocationAccuracy.toLocationRequestQuality(): Int = when (this) {
LocationAccuracy.BestForNavigation, LocationAccuracy.High -> LocationRequestCompat.QUALITY_HIGH_ACCURACY
LocationAccuracy.Balanced -> LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY
LocationAccuracy.Low, LocationAccuracy.Lowest -> LocationRequestCompat.QUALITY_LOW_POWER
}
class NotGrantedLocationProvider : LocationProvider {
override fun updates(request: LocationRequest): Flow<LocationEvent> {
return flowOf(LocationEvent.Unavailable(LocationUnavailableReason.PermissionDenied))
}
}
@Composable
fun rememberNotGrantedLocationProvider(): NotGrantedLocationProvider {
return remember { NotGrantedLocationProvider() }
}
@@ -1,129 +0,0 @@
/*
* 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.userlocation
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.location.LocationManager
import android.os.Handler
import android.os.HandlerThread
import androidx.annotation.RequiresPermission
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.core.location.LocationListenerCompat
import androidx.core.location.LocationManagerCompat
import androidx.core.location.LocationRequestCompat
import androidx.core.os.ExecutorCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.stateIn
import org.maplibre.compose.location.DesiredAccuracy
import org.maplibre.compose.location.Location
import org.maplibre.compose.location.LocationProvider
import org.maplibre.compose.location.PermissionException
import org.maplibre.compose.location.asMapLibreLocation
import org.maplibre.spatialk.units.Length
import org.maplibre.spatialk.units.extensions.inMeters
import kotlin.time.Duration
@SuppressLint("InlinedApi")
class PlatformLocationProvider(
context: Context,
private val updateInterval: Duration,
private val minDistance: Length,
private val desiredAccuracy: DesiredAccuracy = DesiredAccuracy.High,
coroutineScope: CoroutineScope,
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
) : LocationProvider {
override val location: StateFlow<Location?>
init {
if (!handlerThread.isAlive) handlerThread.start()
if (
context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
) {
throw PermissionException()
}
val locationManager = context.getSystemService(LocationManager::class.java)
val provider = PROVIDERS_BY_PRIORITY.firstOrNull { LocationManagerCompat.hasProvider(locationManager, it) }
val locationFlow = if (provider != null) {
createProviderFlow(locationManager, provider)
} else {
emptyFlow()
}
location = locationFlow.stateIn(coroutineScope, sharingStarted, null)
}
@RequiresPermission(
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
)
private fun createProviderFlow(locationManager: LocationManager, provider: String) = callbackFlow {
send(locationManager.getLastKnownLocation(provider)?.asMapLibreLocation())
val listener = LocationListenerCompat { trySend(it.asMapLibreLocation()) }
val request = LocationRequestCompat.Builder(updateInterval.inWholeMilliseconds)
.setQuality(desiredAccuracy.toLocationRequestQuality())
.setMinUpdateDistanceMeters(minDistance.inMeters.toFloat())
.build()
LocationManagerCompat.requestLocationUpdates(
locationManager,
provider,
request,
ExecutorCompat.create(Handler(handlerThread.looper)),
listener,
)
awaitClose { LocationManagerCompat.removeUpdates(locationManager, listener) }
}
private companion object {
private val PROVIDERS_BY_PRIORITY = listOf(
LocationManager.FUSED_PROVIDER,
LocationManager.GPS_PROVIDER,
LocationManager.NETWORK_PROVIDER,
)
private val handlerThread by lazy { HandlerThread("PlatformLocationProvider") }
}
}
private fun DesiredAccuracy.toLocationRequestQuality(): Int = when (this) {
DesiredAccuracy.Highest, DesiredAccuracy.High -> LocationRequestCompat.QUALITY_HIGH_ACCURACY
DesiredAccuracy.Balanced -> LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY
DesiredAccuracy.Low, DesiredAccuracy.Lowest -> LocationRequestCompat.QUALITY_LOW_POWER
}
@Composable
@RequiresPermission(
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
)
fun rememberPlatformLocationProvider(
updateInterval: Duration,
minDistance: Length,
desiredAccuracy: DesiredAccuracy = DesiredAccuracy.High,
context: Context = LocalContext.current,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
sharingStarted: SharingStarted = SharingStarted.WhileSubscribed(stopTimeoutMillis = 1000),
): PlatformLocationProvider {
return remember(context, updateInterval, minDistance, desiredAccuracy, coroutineScope, sharingStarted) {
PlatformLocationProvider(
context = context,
updateInterval = updateInterval,
minDistance = minDistance,
desiredAccuracy = desiredAccuracy,
coroutineScope = coroutineScope,
sharingStarted = sharingStarted,
)
}
}
@@ -8,8 +8,10 @@
package io.element.android.features.location.impl.common.userlocation package io.element.android.features.location.impl.common.userlocation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import org.maplibre.compose.location.Location import org.maplibre.compose.location.Location
@Immutable
data class UserLocationState(val location: Location?) { data class UserLocationState(val location: Location?) {
fun interface Factory { fun interface Factory {
@Composable @Composable
@@ -14,7 +14,7 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
import android.os.IBinder import android.os.IBinder
import androidx.core.app.ServiceCompat import androidx.core.app.ServiceCompat
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.location.impl.common.userlocation.PlatformLocationProvider import io.element.android.features.location.impl.common.userlocation.createLocationProvider
import io.element.android.features.location.impl.di.LocationBindings import io.element.android.features.location.impl.di.LocationBindings
import io.element.android.features.location.impl.live.notification.LiveLocationSharingNotificationCreator import io.element.android.features.location.impl.live.notification.LiveLocationSharingNotificationCreator
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
@@ -30,15 +30,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.maplibre.compose.location.DesiredAccuracy import org.maplibre.compose.location.LocationAccuracy
import org.maplibre.compose.location.PermissionException import org.maplibre.compose.location.LocationEvent
import org.maplibre.compose.location.LocationRequest
import org.maplibre.compose.location.LocationUnavailableReason
import org.maplibre.spatialk.units.extensions.inMeters import org.maplibre.spatialk.units.extensions.inMeters
import org.maplibre.spatialk.units.extensions.meters import org.maplibre.spatialk.units.extensions.meters
import timber.log.Timber import timber.log.Timber
@@ -91,31 +90,38 @@ class LiveLocationSharingService : Service() {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
private fun startLocationUpdatesListener() { private fun startLocationUpdatesListener() {
Timber.d("LiveLocationSharingService listening to location updates") Timber.d("LiveLocationSharingService listening to location updates")
val locationProvider = createLocationProvider(context = applicationContext)
appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow() appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow()
.flatMapLatest { minDistanceMeters -> .flatMapLatest { minDistanceMeters ->
try { val locationRequest = LocationRequest(
PlatformLocationProvider( accuracy = LocationAccuracy.Balanced,
context = applicationContext, minimumInterval = UPDATE_INTERVAL_IN_SECOND.seconds,
updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds, minimumDistance = minDistanceMeters.meters
minDistance = minDistanceMeters.meters, )
desiredAccuracy = DesiredAccuracy.Balanced, locationProvider.updates(locationRequest)
coroutineScope = coroutineScope }
).location .onEach { locationEvent ->
} catch (exception: PermissionException) { when (locationEvent) {
Timber.e(exception, "Failed to create PlatformLocationProvider") is LocationEvent.Fix -> {
coordinator.dispatchUnrecoverableError() val location = ApiLocation(
emptyFlow() lat = locationEvent.location.position.value.latitude,
lon = locationEvent.location.position.value.longitude,
accuracy = locationEvent.location.position.accuracy?.inMeters?.toFloat(),
)
coordinator.dispatch(location)
}
is LocationEvent.Unavailable -> {
when (locationEvent.reason) {
LocationUnavailableReason.ServicesDisabled,
LocationUnavailableReason.TemporarilyUnavailable -> Unit
LocationUnavailableReason.Unsupported,
LocationUnavailableReason.Misconfigured,
LocationUnavailableReason.PermissionDenied,
LocationUnavailableReason.UnexpectedFailure -> coordinator.dispatchUnrecoverableError()
}
}
} }
} }
.filterNotNull()
.map { location ->
ApiLocation(
lat = location.position.value.latitude,
lon = location.position.value.longitude,
accuracy = location.position.accuracy?.inMeters?.toFloat(),
)
}
.onEach(coordinator::dispatch)
.launchIn(coroutineScope) .launchIn(coroutineScope)
} }
@@ -107,6 +107,7 @@ fun ShareLocationView(
val scaffoldState = rememberBottomSheetScaffoldState( val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberBottomSheetState( bottomSheetState = rememberBottomSheetState(
initialValue = SheetValue.Expanded, initialValue = SheetValue.Expanded,
enabledValues = setOf(SheetValue.PartiallyExpanded, SheetValue.Expanded),
) )
) )
val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition) val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition)
@@ -94,6 +94,7 @@ fun ShowLocationView(
val scaffoldState = rememberBottomSheetScaffoldState( val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberBottomSheetState( bottomSheetState = rememberBottomSheetState(
initialValue = SheetValue.Expanded, initialValue = SheetValue.Expanded,
enabledValues = setOf(SheetValue.PartiallyExpanded, SheetValue.Expanded),
) )
) )
LaunchedEffect(state.isSheetDraggable) { LaunchedEffect(state.isSheetDraggable) {
@@ -154,7 +155,6 @@ fun ShowLocationView(
modifier = Modifier.clickable { modifier = Modifier.clickable {
state.eventSink(ShowLocationEvent.TrackMyLocation(false)) state.eventSink(ShowLocationEvent.TrackMyLocation(false))
val position = CameraPosition( val position = CameraPosition(
padding = sheetPaddings,
target = Position(locationShare.location.lon, locationShare.location.lat), target = Position(locationShare.location.lon, locationShare.location.lat),
// Force pointing to NORTH // Force pointing to NORTH
bearing = 0.0, bearing = 0.0,
+4 -4
View File
@@ -49,6 +49,7 @@ sqldelight = "2.3.2"
wysiwyg = "2.42.1" wysiwyg = "2.42.1"
telephoto = "0.19.0" telephoto = "0.19.0"
haze = "1.7.2" haze = "1.7.2"
maplibre-compose = "0.15.0"
# Dependency analysis # Dependency analysis
dependencyAnalysis = "3.19.1" dependencyAnalysis = "3.19.1"
@@ -215,10 +216,9 @@ vanniktech_blurhash = "com.vanniktech:blurhash:0.3.0"
telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil3", version.ref = "telephoto" } telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil3", version.ref = "telephoto" }
telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" } telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
statemachine = "com.freeletics.flowredux:compose:1.2.2" statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre = "org.maplibre.gl:android-sdk:13.5.0" maplibre_compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" }
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2" maplibre_compose_runtime_vulkan = { module = "org.maplibre.compose:maplibre-compose-runtime-vulkan-android", version.ref = "maplibre-compose" }
maplibre_compose = "org.maplibre.compose:maplibre-compose:0.14.0" maplibre_compose_location_runtime_gms = { module = "org.maplibre.compose:location-runtime-gms", version.ref = "maplibre-compose" }
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
opusencoder = "io.element.android:opusencoder:1.2.0" opusencoder = "io.element.android:opusencoder:1.2.0"
zxing_cpp = "io.github.zxing-cpp:android:3.1.1" zxing_cpp = "io.github.zxing-cpp:android:3.1.1"
google_zxing = "com.google.zxing:core:3.5.4" google_zxing = "com.google.zxing:core:3.5.4"
+1 -3
View File
@@ -18,7 +18,6 @@ uses-permission: name='android.permission.USE_FULL_SCREEN_INTENT'
uses-permission: name='android.permission.ACCESS_NETWORK_STATE' uses-permission: name='android.permission.ACCESS_NETWORK_STATE'
uses-permission: name='android.permission.VIBRATE' uses-permission: name='android.permission.VIBRATE'
uses-permission: name='android.permission.ACCESS_LOCAL_NETWORK' uses-permission: name='android.permission.ACCESS_LOCAL_NETWORK'
uses-permission: name='android.permission.ACCESS_WIFI_STATE'
uses-permission: name='android.permission.USE_BIOMETRIC' uses-permission: name='android.permission.USE_BIOMETRIC'
uses-permission: name='android.permission.USE_FINGERPRINT' uses-permission: name='android.permission.USE_FINGERPRINT'
uses-permission: name='com.google.android.c2dm.permission.RECEIVE' uses-permission: name='com.google.android.c2dm.permission.RECEIVE'
@@ -81,8 +80,7 @@ uses-library-not-required:'androidx.window.sidecar'
feature-group: label='' feature-group: label=''
uses-feature-not-required: name='android.hardware.camera' uses-feature-not-required: name='android.hardware.camera'
uses-feature-not-required: name='android.hardware.microphone' uses-feature-not-required: name='android.hardware.microphone'
uses-feature-not-required: name='android.hardware.vulkan.version' version='4194307' uses-feature-not-required: name='android.hardware.vulkan.version'
uses-feature-not-required: name='android.hardware.wifi'
uses-feature: name='android.hardware.faketouch' uses-feature: name='android.hardware.faketouch'
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
uses-feature: name='android.hardware.location' uses-feature: name='android.hardware.location'