Support Google Play Services fused location backend

This commit is contained in:
ganfra
2026-09-07 18:11:38 +02:00
parent 3754258d88
commit f72e1ad2cc
6 changed files with 44 additions and 37 deletions
+4
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)
@@ -25,7 +25,7 @@ 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) {
rememberPlatformLocationProvider() rememberLocationProvider()
} else { } else {
rememberNotGrantedLocationProvider() rememberNotGrantedLocationProvider()
} }
@@ -26,9 +26,12 @@ 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 io.element.android.libraries.core.data.tryOrNull
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow 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.LocationAccuracy
import org.maplibre.compose.location.LocationEvent import org.maplibre.compose.location.LocationEvent
import org.maplibre.compose.location.LocationProvider import org.maplibre.compose.location.LocationProvider
@@ -36,11 +39,33 @@ import org.maplibre.compose.location.LocationRequest
import org.maplibre.compose.location.LocationUnavailableReason import org.maplibre.compose.location.LocationUnavailableReason
import org.maplibre.compose.location.asMapLibreLocation import org.maplibre.compose.location.asMapLibreLocation
import org.maplibre.spatialk.units.extensions.inMeters 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( class PlatformLocationProvider(
private val context: Context, private val context: Context,
) : LocationProvider { ) : LocationProvider {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
override fun updates(request: LocationRequest): Flow<LocationEvent> = callbackFlow { override fun updates(request: LocationRequest): Flow<LocationEvent> = callbackFlow {
if (!context.hasLocationPermission()) { if (!context.hasLocationPermission()) {
@@ -58,6 +83,7 @@ class PlatformLocationProvider(
trySend(LocationEvent.Unavailable(LocationUnavailableReason.ServicesDisabled)) trySend(LocationEvent.Unavailable(LocationUnavailableReason.ServicesDisabled))
} }
} }
var registered = false var registered = false
fun refreshRegistration() { fun refreshRegistration() {
@@ -126,7 +152,7 @@ class PlatformLocationProvider(
close() close()
} }
awaitClose { awaitClose {
runCatching { context.unregisterReceiver(settingsReceiver) } tryOrNull { context.unregisterReceiver(settingsReceiver) }
LocationManagerCompat.removeUpdates(locationManager, listener) LocationManagerCompat.removeUpdates(locationManager, listener)
} }
} }
@@ -155,9 +181,13 @@ private fun LocationAccuracy.toLocationRequestQuality(): Int = when (this) {
LocationAccuracy.Low, LocationAccuracy.Lowest -> LocationRequestCompat.QUALITY_LOW_POWER LocationAccuracy.Low, LocationAccuracy.Lowest -> LocationRequestCompat.QUALITY_LOW_POWER
} }
@Composable class NotGrantedLocationProvider : LocationProvider {
fun rememberPlatformLocationProvider(context: Context = LocalContext.current): PlatformLocationProvider { override fun updates(request: LocationRequest): Flow<LocationEvent> {
return remember(context) { return flowOf(LocationEvent.Unavailable(LocationUnavailableReason.PermissionDenied))
PlatformLocationProvider(context = context)
} }
} }
@Composable
fun rememberNotGrantedLocationProvider(): NotGrantedLocationProvider {
return remember { NotGrantedLocationProvider() }
}
@@ -1,28 +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 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() }
}
@@ -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
@@ -90,7 +90,7 @@ 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) val locationProvider = createLocationProvider(context = applicationContext)
appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow() appPreferencesStore.getLiveLocationMinimumDistanceInMetersUpdateFlow()
.flatMapLatest { minDistanceMeters -> .flatMapLatest { minDistanceMeters ->
val locationRequest = LocationRequest( val locationRequest = LocationRequest(
+1
View File
@@ -218,6 +218,7 @@ telephoto_flick = { module = "me.saket.telephoto:flick-android", version.ref = "
statemachine = "com.freeletics.flowredux:compose:1.2.2" statemachine = "com.freeletics.flowredux:compose:1.2.2"
maplibre_compose = { module = "org.maplibre.compose:maplibre-compose", version.ref = "maplibre-compose" } 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_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" 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"