mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Merge pull request #7663 from element-hq/feature/fga/maplibre-compose-update
Update maplibre compose to 0.15
This commit is contained in:
@@ -288,6 +288,10 @@ dependencies {
|
||||
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.androidx.splash)
|
||||
implementation(libs.androidx.core)
|
||||
@@ -325,6 +329,7 @@ licensee {
|
||||
allow("BSD-3-Clause")
|
||||
allow("EPL-1.0")
|
||||
allowUrl("https://opensource.org/license/bsd-3-clause")
|
||||
allowUrl("https://opensource.org/license/bsd-2-clause")
|
||||
allowUrl("https://opensource.org/licenses/MIT")
|
||||
allowUrl("https://developer.android.com/studio/terms.html")
|
||||
allowUrl("https://www.zetetic.net/sqlcipher/license/")
|
||||
|
||||
@@ -29,6 +29,7 @@ dependencies {
|
||||
implementation(projects.features.enterprise.api)
|
||||
implementation(projects.features.messages.api)
|
||||
implementation(libs.maplibre.compose)
|
||||
runtimeOnly(libs.maplibre.compose.runtime.vulkan)
|
||||
implementation(libs.coil)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
|
||||
-10
@@ -9,11 +9,9 @@
|
||||
package io.element.android.features.location.impl.common
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.ui.Alignment
|
||||
import org.maplibre.compose.camera.CameraPosition
|
||||
import org.maplibre.compose.map.GestureOptions
|
||||
import org.maplibre.compose.map.MapOptions
|
||||
import org.maplibre.compose.map.OrnamentOptions
|
||||
import org.maplibre.compose.map.RenderOptions
|
||||
import org.maplibre.spatialk.geojson.Position
|
||||
|
||||
@@ -24,14 +22,6 @@ object MapDefaults {
|
||||
val options = MapOptions(
|
||||
renderOptions = RenderOptions.Standard,
|
||||
gestureOptions = GestureOptions.Standard,
|
||||
ornamentOptions = OrnamentOptions(
|
||||
isLogoEnabled = true,
|
||||
logoAlignment = Alignment.BottomStart,
|
||||
isAttributionEnabled = true,
|
||||
attributionAlignment = Alignment.BottomEnd,
|
||||
isCompassEnabled = false,
|
||||
isScaleBarEnabled = false,
|
||||
)
|
||||
)
|
||||
|
||||
val defaultCameraPosition = CameraPosition(
|
||||
|
||||
+21
-7
@@ -7,16 +7,19 @@
|
||||
|
||||
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.BoxScope
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.only
|
||||
@@ -32,7 +35,6 @@ import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.rememberBottomSheetScaffoldState
|
||||
import androidx.compose.material3.rememberBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
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.map.MapOptions
|
||||
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.util.MaplibreComposable
|
||||
import kotlin.math.roundToInt
|
||||
@@ -115,10 +120,6 @@ fun MapBottomSheetScaffold(
|
||||
PaddingValues(bottom = bottomPadding)
|
||||
}
|
||||
}
|
||||
// Update camera position when sheet padding changes
|
||||
LaunchedEffect(sheetPadding) {
|
||||
cameraState.position = cameraState.position.copy(padding = sheetPadding)
|
||||
}
|
||||
BottomSheetScaffold(
|
||||
modifier = Modifier,
|
||||
sheetPeekHeight = sheetPeekHeight,
|
||||
@@ -135,14 +136,27 @@ fun MapBottomSheetScaffold(
|
||||
snackbarHost = snackbarHost,
|
||||
topBar = topBar,
|
||||
) {
|
||||
val ornamentOptions = mapOptions.ornamentOptions.copy(padding = sheetPadding)
|
||||
val mapOptions = mapOptions.copy(ornamentOptions = ornamentOptions)
|
||||
Box {
|
||||
when (customMapTilerConfig) {
|
||||
is AsyncData.Success -> {
|
||||
MaplibreMap(
|
||||
options = mapOptions,
|
||||
cameraPadding = sheetPadding,
|
||||
contentWindowInsets = WindowInsets(bottom = sheetPadding.calculateBottomPadding()),
|
||||
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(),
|
||||
cameraState = cameraState,
|
||||
content = mapContent,
|
||||
|
||||
+24
-12
@@ -7,14 +7,16 @@
|
||||
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.rememberNullLocationProvider
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
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 kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@@ -22,17 +24,27 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class DefaultUserLocationStateFactory : UserLocationState.Factory {
|
||||
@Composable
|
||||
override fun create(hasLocationPermission: Boolean): UserLocationState {
|
||||
val locationProvider = if (!hasLocationPermission) {
|
||||
rememberNullLocationProvider()
|
||||
val locationProvider = if (hasLocationPermission) {
|
||||
rememberLocationProvider()
|
||||
} else {
|
||||
@SuppressLint("MissingPermission")
|
||||
rememberPlatformLocationProvider(
|
||||
updateInterval = 5.seconds,
|
||||
desiredAccuracy = DesiredAccuracy.High,
|
||||
minDistance = 5.meters,
|
||||
)
|
||||
rememberNotGrantedLocationProvider()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+191
@@ -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() }
|
||||
}
|
||||
-129
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
@@ -8,8 +8,10 @@
|
||||
package io.element.android.features.location.impl.common.userlocation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import org.maplibre.compose.location.Location
|
||||
|
||||
@Immutable
|
||||
data class UserLocationState(val location: Location?) {
|
||||
fun interface Factory {
|
||||
@Composable
|
||||
|
||||
+33
-27
@@ -14,7 +14,7 @@ import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.ServiceCompat
|
||||
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.live.notification.LiveLocationSharingNotificationCreator
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
@@ -30,15 +30,14 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.maplibre.compose.location.DesiredAccuracy
|
||||
import org.maplibre.compose.location.PermissionException
|
||||
import org.maplibre.compose.location.LocationAccuracy
|
||||
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.meters
|
||||
import timber.log.Timber
|
||||
@@ -91,31 +90,38 @@ class LiveLocationSharingService : Service() {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun startLocationUpdatesListener() {
|
||||
Timber.d("LiveLocationSharingService listening to location updates")
|
||||
val locationProvider = createLocationProvider(context = applicationContext)
|
||||
appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow()
|
||||
.flatMapLatest { minDistanceMeters ->
|
||||
try {
|
||||
PlatformLocationProvider(
|
||||
context = applicationContext,
|
||||
updateInterval = UPDATE_INTERVAL_IN_SECOND.seconds,
|
||||
minDistance = minDistanceMeters.meters,
|
||||
desiredAccuracy = DesiredAccuracy.Balanced,
|
||||
coroutineScope = coroutineScope
|
||||
).location
|
||||
} catch (exception: PermissionException) {
|
||||
Timber.e(exception, "Failed to create PlatformLocationProvider")
|
||||
coordinator.dispatchUnrecoverableError()
|
||||
emptyFlow()
|
||||
val locationRequest = LocationRequest(
|
||||
accuracy = LocationAccuracy.Balanced,
|
||||
minimumInterval = UPDATE_INTERVAL_IN_SECOND.seconds,
|
||||
minimumDistance = minDistanceMeters.meters
|
||||
)
|
||||
locationProvider.updates(locationRequest)
|
||||
}
|
||||
.onEach { locationEvent ->
|
||||
when (locationEvent) {
|
||||
is LocationEvent.Fix -> {
|
||||
val location = ApiLocation(
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -107,6 +107,7 @@ fun ShareLocationView(
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberBottomSheetState(
|
||||
initialValue = SheetValue.Expanded,
|
||||
enabledValues = setOf(SheetValue.PartiallyExpanded, SheetValue.Expanded),
|
||||
)
|
||||
)
|
||||
val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition)
|
||||
|
||||
+1
-1
@@ -94,6 +94,7 @@ fun ShowLocationView(
|
||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = rememberBottomSheetState(
|
||||
initialValue = SheetValue.Expanded,
|
||||
enabledValues = setOf(SheetValue.PartiallyExpanded, SheetValue.Expanded),
|
||||
)
|
||||
)
|
||||
LaunchedEffect(state.isSheetDraggable) {
|
||||
@@ -154,7 +155,6 @@ fun ShowLocationView(
|
||||
modifier = Modifier.clickable {
|
||||
state.eventSink(ShowLocationEvent.TrackMyLocation(false))
|
||||
val position = CameraPosition(
|
||||
padding = sheetPaddings,
|
||||
target = Position(locationShare.location.lon, locationShare.location.lat),
|
||||
// Force pointing to NORTH
|
||||
bearing = 0.0,
|
||||
|
||||
@@ -49,6 +49,7 @@ sqldelight = "2.3.2"
|
||||
wysiwyg = "2.42.1"
|
||||
telephoto = "0.19.0"
|
||||
haze = "1.7.2"
|
||||
maplibre-compose = "0.15.0"
|
||||
|
||||
# Dependency analysis
|
||||
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_flick = { module = "me.saket.telephoto:flick-android", version.ref = "telephoto" }
|
||||
statemachine = "com.freeletics.flowredux:compose:1.2.2"
|
||||
maplibre = "org.maplibre.gl:android-sdk:13.5.0"
|
||||
maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:3.0.2"
|
||||
maplibre_compose = "org.maplibre.compose:maplibre-compose:0.14.0"
|
||||
maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:3.0.2"
|
||||
maplibre_compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" }
|
||||
maplibre_compose_runtime_vulkan = { module = "org.maplibre.compose:maplibre-compose-runtime-vulkan-android", version.ref = "maplibre-compose" }
|
||||
maplibre_compose_location_runtime_gms = { module = "org.maplibre.compose:location-runtime-gms", version.ref = "maplibre-compose" }
|
||||
opusencoder = "io.element.android:opusencoder:1.2.0"
|
||||
zxing_cpp = "io.github.zxing-cpp:android:3.1.1"
|
||||
google_zxing = "com.google.zxing:core:3.5.4"
|
||||
|
||||
@@ -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.VIBRATE'
|
||||
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_FINGERPRINT'
|
||||
uses-permission: name='com.google.android.c2dm.permission.RECEIVE'
|
||||
@@ -81,8 +80,7 @@ uses-library-not-required:'androidx.window.sidecar'
|
||||
feature-group: label=''
|
||||
uses-feature-not-required: name='android.hardware.camera'
|
||||
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.wifi'
|
||||
uses-feature-not-required: name='android.hardware.vulkan.version'
|
||||
uses-feature: name='android.hardware.faketouch'
|
||||
uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps'
|
||||
uses-feature: name='android.hardware.location'
|
||||
|
||||
Reference in New Issue
Block a user