tilfluktsrom/app/src/standard/java/no/naiv/tilfluktsrom/location/LocationProvider.kt
Ole-Morten Duesund ff4b3245f5 Legg til fdroid-byggvariant uten Google Play Services
Splitt LocationProvider, ShelterWidgetProvider og WidgetUpdateWorker i
to varianter: standard (med Play Services for bedre GPS) og fdroid
(kun AOSP LocationManager). Play Services-avhengigheten er nå begrenset
til standardImplementation.

Begge varianter bygger og har identisk funksjonalitet — fdroid-varianten
mangler bare FusedLocationProviderClient som en ekstra lokasjonskilde.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:25:47 +01:00

203 lines
8.1 KiB
Kotlin

package no.naiv.tilfluktsrom.location
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
/**
* Provides GPS location updates with automatic fallback.
*
* Uses FusedLocationProviderClient when Google Play Services is available (most devices),
* and falls back to LocationManager (GPS + Network providers) for degoogled/F-Droid devices.
*
* The public API is identical regardless of backend: locationUpdates() emits a Flow<Location>.
*/
class LocationProvider(private val context: Context) {
companion object {
private const val TAG = "LocationProvider"
private const val UPDATE_INTERVAL_MS = 5000L
private const val FASTEST_INTERVAL_MS = 2000L
}
/** Checked once at construction — Play Services availability won't change at runtime. */
private val usePlayServices: Boolean = isPlayServicesAvailable()
init {
Log.d(TAG, "Location backend: ${if (usePlayServices) "Play Services" else "LocationManager"}")
}
/**
* Stream of location updates. Emits the last known location first (if available),
* then continuous updates. Throws SecurityException if permission is not granted.
*/
fun locationUpdates(): Flow<Location> = callbackFlow {
if (!hasLocationPermission()) {
close(SecurityException("Location permission not granted"))
return@callbackFlow
}
if (usePlayServices) {
val fusedClient: FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
// Emit last known location immediately for fast first display
try {
fusedClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
val result = trySend(location)
if (result.isFailure) {
Log.w(TAG, "Failed to emit last known location")
}
}
}
.addOnFailureListener { e ->
Log.w(TAG, "Could not get last known location", e)
}
} catch (e: SecurityException) {
Log.e(TAG, "SecurityException getting last location", e)
}
val locationRequest = LocationRequest.Builder(
Priority.PRIORITY_HIGH_ACCURACY,
UPDATE_INTERVAL_MS
).apply {
setMinUpdateIntervalMillis(FASTEST_INTERVAL_MS)
setWaitForAccurateLocation(false)
}.build()
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let { location ->
val sendResult = trySend(location)
if (sendResult.isFailure) {
Log.w(TAG, "Failed to emit location update")
}
}
}
}
try {
fusedClient.requestLocationUpdates(
locationRequest,
callback,
Looper.getMainLooper()
)
} catch (e: SecurityException) {
Log.e(TAG, "SecurityException requesting location updates", e)
close(e)
return@callbackFlow
}
awaitClose {
fusedClient.removeLocationUpdates(callback)
}
} else {
// Fallback: LocationManager for devices without Play Services
val locationManager = context.getSystemService(Context.LOCATION_SERVICE)
as? LocationManager
if (locationManager == null) {
close(IllegalStateException("LocationManager not available"))
return@callbackFlow
}
// Emit best last known location immediately (pick most recent of GPS/Network)
try {
val lastGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
val lastNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER)
val best = listOfNotNull(lastGps, lastNetwork).maxByOrNull { it.time }
if (best != null) {
val result = trySend(best)
if (result.isFailure) {
Log.w(TAG, "Failed to emit last known location (fallback)")
}
}
} catch (e: SecurityException) {
Log.e(TAG, "SecurityException getting last known location (fallback)", e)
}
// LocationListener compatible with API 26-28 (onStatusChanged required before API 29)
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
val sendResult = trySend(location)
if (sendResult.isFailure) {
Log.w(TAG, "Failed to emit location update (fallback)")
}
}
// Required for API 26-28 compatibility (deprecated from API 29+)
@Deprecated("Deprecated in API 29")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
try {
// Request from both providers: GPS is accurate, Network gives faster first fix
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
UPDATE_INTERVAL_MS,
0f,
listener,
Looper.getMainLooper()
)
}
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
UPDATE_INTERVAL_MS,
0f,
listener,
Looper.getMainLooper()
)
}
} catch (e: SecurityException) {
Log.e(TAG, "SecurityException requesting location updates (fallback)", e)
close(e)
return@callbackFlow
}
awaitClose {
locationManager.removeUpdates(listener)
}
}
}
fun hasLocationPermission(): Boolean {
return ContextCompat.checkSelfPermission(
context, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
private fun isPlayServicesAvailable(): Boolean {
return try {
val result = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(context)
result == ConnectionResult.SUCCESS
} catch (e: Exception) {
// Play Services library might not even be resolvable on some ROMs
false
}
}
}