mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-04 10:32:34 +00:00
Adapt location feature to updated maplibre-compose API
This commit is contained in:
-10
@@ -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(
|
||||||
|
|||||||
+21
-7
@@ -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,
|
||||||
|
|||||||
+24
-12
@@ -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()
|
rememberPlatformLocationProvider()
|
||||||
} 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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 androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.maplibre.compose.location.LocationEvent
|
||||||
|
import org.maplibre.compose.location.LocationProvider
|
||||||
|
import org.maplibre.compose.location.LocationRequest
|
||||||
|
import org.maplibre.compose.location.LocationUnavailableReason
|
||||||
|
|
||||||
|
class NotGrantedLocationProvider : LocationProvider {
|
||||||
|
override fun updates(request: LocationRequest): Flow<LocationEvent> {
|
||||||
|
return flowOf(LocationEvent.Unavailable(LocationUnavailableReason.PermissionDenied))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberNotGrantedLocationProvider(): NotGrantedLocationProvider {
|
||||||
|
return remember { NotGrantedLocationProvider() }
|
||||||
|
}
|
||||||
+112
-78
@@ -9,83 +9,126 @@ package io.element.android.features.location.impl.common.userlocation
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.location.Location
|
||||||
import android.location.LocationManager
|
import android.location.LocationManager
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.HandlerThread
|
import android.os.HandlerThread
|
||||||
import androidx.annotation.RequiresPermission
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.location.LocationListenerCompat
|
import androidx.core.location.LocationListenerCompat
|
||||||
import androidx.core.location.LocationManagerCompat
|
import androidx.core.location.LocationManagerCompat
|
||||||
import androidx.core.location.LocationRequestCompat
|
import androidx.core.location.LocationRequestCompat
|
||||||
import androidx.core.os.ExecutorCompat
|
import androidx.core.os.ExecutorCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import org.maplibre.compose.location.LocationAccuracy
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import org.maplibre.compose.location.LocationEvent
|
||||||
import org.maplibre.compose.location.DesiredAccuracy
|
|
||||||
import org.maplibre.compose.location.Location
|
|
||||||
import org.maplibre.compose.location.LocationProvider
|
import org.maplibre.compose.location.LocationProvider
|
||||||
import org.maplibre.compose.location.PermissionException
|
import org.maplibre.compose.location.LocationRequest
|
||||||
|
import org.maplibre.compose.location.LocationUnavailableReason
|
||||||
import org.maplibre.compose.location.asMapLibreLocation
|
import org.maplibre.compose.location.asMapLibreLocation
|
||||||
import org.maplibre.spatialk.units.Length
|
|
||||||
import org.maplibre.spatialk.units.extensions.inMeters
|
import org.maplibre.spatialk.units.extensions.inMeters
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
@SuppressLint("InlinedApi")
|
|
||||||
class PlatformLocationProvider(
|
class PlatformLocationProvider(
|
||||||
context: Context,
|
private val 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 {
|
) : LocationProvider {
|
||||||
override val location: StateFlow<Location?>
|
|
||||||
|
|
||||||
init {
|
@SuppressLint("MissingPermission")
|
||||||
if (!handlerThread.isAlive) handlerThread.start()
|
override fun updates(request: LocationRequest): Flow<LocationEvent> = callbackFlow {
|
||||||
if (
|
if (!context.hasLocationPermission()) {
|
||||||
context.checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
|
trySend(LocationEvent.Unavailable(LocationUnavailableReason.PermissionDenied))
|
||||||
context.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
|
close()
|
||||||
) {
|
return@callbackFlow
|
||||||
throw PermissionException()
|
|
||||||
}
|
}
|
||||||
val locationManager = context.getSystemService(LocationManager::class.java)
|
val locationManager = context.getSystemService(LocationManager::class.java)
|
||||||
val provider = PROVIDERS_BY_PRIORITY.firstOrNull { LocationManagerCompat.hasProvider(locationManager, it) }
|
val listener = object : LocationListenerCompat {
|
||||||
val locationFlow = if (provider != null) {
|
override fun onLocationChanged(location: Location) {
|
||||||
createProviderFlow(locationManager, provider)
|
trySend(LocationEvent.Fix(location.asMapLibreLocation()))
|
||||||
} else {
|
}
|
||||||
emptyFlow()
|
|
||||||
}
|
|
||||||
location = locationFlow.stateIn(coroutineScope, sharingStarted, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresPermission(
|
override fun onProviderDisabled(provider: String) {
|
||||||
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
|
trySend(LocationEvent.Unavailable(LocationUnavailableReason.ServicesDisabled))
|
||||||
)
|
}
|
||||||
private fun createProviderFlow(locationManager: LocationManager, provider: String) = callbackFlow {
|
}
|
||||||
send(locationManager.getLastKnownLocation(provider)?.asMapLibreLocation())
|
var registered = false
|
||||||
val listener = LocationListenerCompat { trySend(it.asMapLibreLocation()) }
|
|
||||||
val request = LocationRequestCompat.Builder(updateInterval.inWholeMilliseconds)
|
fun refreshRegistration() {
|
||||||
.setQuality(desiredAccuracy.toLocationRequestQuality())
|
if (registered) {
|
||||||
.setMinUpdateDistanceMeters(minDistance.inMeters.toFloat())
|
LocationManagerCompat.removeUpdates(locationManager, listener)
|
||||||
.build()
|
registered = false
|
||||||
LocationManagerCompat.requestLocationUpdates(
|
}
|
||||||
locationManager,
|
if (!LocationManagerCompat.isLocationEnabled(locationManager)) {
|
||||||
provider,
|
trySend(LocationEvent.Unavailable(LocationUnavailableReason.ServicesDisabled))
|
||||||
request,
|
return
|
||||||
ExecutorCompat.create(Handler(handlerThread.looper)),
|
}
|
||||||
listener,
|
val provider = PROVIDERS_BY_PRIORITY.firstOrNull { LocationManagerCompat.hasProvider(locationManager, it) }
|
||||||
)
|
if (provider == null) {
|
||||||
awaitClose { LocationManagerCompat.removeUpdates(locationManager, listener) }
|
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 {
|
||||||
|
runCatching { context.unregisterReceiver(settingsReceiver) }
|
||||||
|
LocationManagerCompat.removeUpdates(locationManager, listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
@@ -94,36 +137,27 @@ class PlatformLocationProvider(
|
|||||||
LocationManager.GPS_PROVIDER,
|
LocationManager.GPS_PROVIDER,
|
||||||
LocationManager.NETWORK_PROVIDER,
|
LocationManager.NETWORK_PROVIDER,
|
||||||
)
|
)
|
||||||
private val handlerThread by lazy { HandlerThread("PlatformLocationProvider") }
|
private val handlerThread by lazy {
|
||||||
|
HandlerThread("PlatformLocationProvider").apply { start() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DesiredAccuracy.toLocationRequestQuality(): Int = when (this) {
|
private fun Context.hasLocationPermission(): Boolean =
|
||||||
DesiredAccuracy.Highest, DesiredAccuracy.High -> LocationRequestCompat.QUALITY_HIGH_ACCURACY
|
checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) ==
|
||||||
DesiredAccuracy.Balanced -> LocationRequestCompat.QUALITY_BALANCED_POWER_ACCURACY
|
PackageManager.PERMISSION_GRANTED ||
|
||||||
DesiredAccuracy.Low, DesiredAccuracy.Lowest -> LocationRequestCompat.QUALITY_LOW_POWER
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@RequiresPermission(
|
fun rememberPlatformLocationProvider(context: Context = LocalContext.current): PlatformLocationProvider {
|
||||||
anyOf = [Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION]
|
return remember(context) {
|
||||||
)
|
PlatformLocationProvider(context = context)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+32
-26
@@ -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 = PlatformLocationProvider(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-1
@@ -154,7 +154,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,
|
||||||
|
|||||||
Reference in New Issue
Block a user