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>
203 lines
8.1 KiB
Kotlin
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
|
|
}
|
|
}
|
|
}
|