A revoked location permission was previously caught and sent as null with zero logging, making it indistinguishable from "no fix yet" and impossible to diagnose. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
84 lines
2.7 KiB
Kotlin
84 lines
2.7 KiB
Kotlin
package no.naiv.tiltshift.util
|
|
|
|
import android.Manifest
|
|
import android.content.Context
|
|
import android.content.pm.PackageManager
|
|
import android.location.Location
|
|
import android.os.Looper
|
|
import android.util.Log
|
|
import androidx.core.content.ContextCompat
|
|
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 location updates for EXIF GPS tagging.
|
|
*/
|
|
class LocationProvider(private val context: Context) {
|
|
|
|
companion object {
|
|
private const val TAG = "LocationProvider"
|
|
}
|
|
|
|
private val fusedLocationClient: FusedLocationProviderClient =
|
|
LocationServices.getFusedLocationProviderClient(context)
|
|
|
|
/**
|
|
* Returns a Flow of location updates.
|
|
* Updates are throttled to conserve battery - we only need periodic updates for photo tagging.
|
|
*/
|
|
fun locationFlow(): Flow<Location?> = callbackFlow {
|
|
if (!hasLocationPermission()) {
|
|
trySend(null)
|
|
awaitClose()
|
|
return@callbackFlow
|
|
}
|
|
|
|
val locationRequest = LocationRequest.Builder(
|
|
Priority.PRIORITY_BALANCED_POWER_ACCURACY,
|
|
30_000L // Update every 30 seconds
|
|
).apply {
|
|
setMinUpdateIntervalMillis(10_000L)
|
|
setMaxUpdateDelayMillis(60_000L)
|
|
}.build()
|
|
|
|
val callback = object : LocationCallback() {
|
|
override fun onLocationResult(result: LocationResult) {
|
|
result.lastLocation?.let { trySend(it) }
|
|
}
|
|
}
|
|
|
|
try {
|
|
fusedLocationClient.requestLocationUpdates(
|
|
locationRequest,
|
|
callback,
|
|
Looper.getMainLooper()
|
|
)
|
|
|
|
// Also try to get last known location immediately
|
|
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
|
|
location?.let { trySend(it) }
|
|
}
|
|
} catch (e: SecurityException) {
|
|
Log.w(TAG, "Location permission revoked at runtime", e)
|
|
trySend(null)
|
|
}
|
|
|
|
awaitClose {
|
|
fusedLocationClient.removeLocationUpdates(callback)
|
|
}
|
|
}
|
|
|
|
private fun hasLocationPermission(): Boolean {
|
|
return ContextCompat.checkSelfPermission(
|
|
context,
|
|
Manifest.permission.ACCESS_FINE_LOCATION
|
|
) == PackageManager.PERMISSION_GRANTED
|
|
}
|
|
}
|