tilt-shift-camera/app/src/main/java/no/naiv/tiltshift/util/LocationProvider.kt

88 lines
2.8 KiB
Kotlin
Raw Normal View History

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
Fix concurrency, lifecycle, performance, and config issues from audit Concurrency & bitmap lifecycle: - Defer bitmap recycling by one cycle so Compose finishes drawing before native memory is freed (preview bitmaps, thumbnails) - Make galleryPreviewSource @Volatile for cross-thread visibility - Join preview job before recycling source bitmap in cancelGalleryPreview() to prevent use-after-free during CPU blur loop - Add @Volatile to TiltShiftRenderer.currentTexCoords (UI/GL thread race) - Fix error dismiss race with cancellable Job tracking Lifecycle & resource management: - Release GL resources via glSurfaceView.queueEvent (must run on GL thread) - Pause GLSurfaceView when entering gallery preview mode - Shut down captureExecutor in CameraManager.release() (thread leak) - Use WeakReference for lifecycleOwnerRef to avoid Activity GC delay - Fix thumbnail bitmap leak on coroutine cancellation (add to finally) - Guarantee imageProxy.close() in finally block Performance: - Compute gradient mask at 1/4 resolution with bilinear upscale (~93% less per-pixel trig work, ~75% less mask memory) - Precompute cos/sin on CPU, pass as uCosAngle/uSinAngle uniforms (eliminates per-fragment transcendental calls in GLSL) - Unroll 9-tap Gaussian blur kernel (avoids integer-branched weight lookup that de-optimizes on mobile GPUs) - Add 80ms debounce to preview recomputation during slider drags Silent failure fixes: - Check bitmap.compress() return value; report error on failure - Log all loadBitmapFromUri null paths (stream, dimensions, decode) - Surface preview computation errors and ActivityNotFoundException to user - Return boolean from writeExifToUri, log at ERROR level - Wrap gallery preview downscale in try-catch (OOM protection) Config: - Add ACCESS_MEDIA_LOCATION permission (GPS EXIF on Android 10+) - Accept coarse-only location grant for geotags - Remove dead adjustResize (no effect with edge-to-edge) - Set windowBackground to black (eliminates white flash on cold start) - Add values-night theme for dark mode - Remove overly broad ProGuard keeps (CameraX/GMS ship consumer rules) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 13:44:12 +01:00
) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
}