From e9bef0607ffe2967a2578ec85f70405bcb7d2d66 Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Thu, 5 Mar 2026 13:50:33 +0100 Subject: [PATCH] Add GPS geotagging toggle to camera top bar Users can now opt out of embedding GPS coordinates in photos. The toggle is persisted via SharedPreferences and defaults to enabled. When disabled, effectiveLocation returns null so no EXIF GPS tags are written. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 3 +-- .../java/no/naiv/tiltshift/ui/CameraScreen.kt | 17 ++++++++++++++ .../no/naiv/tiltshift/ui/CameraViewModel.kt | 23 +++++++++++++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d9a4edf..22a6b5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ Android camera app that applies a real-time tilt-shift (miniature/diorama) blur - Gesture controls: drag to position, pinch to resize, two-finger rotate - Slider panel for precise control of blur, falloff, size, angle, aspect ratio - Multi-lens support on devices with multiple back cameras -- EXIF GPS tagging from device location +- EXIF GPS tagging with user-toggleable opt-out (persisted across restarts) - Saves processed images to MediaStore (scoped storage) ## Architecture @@ -97,6 +97,5 @@ Bitmaps emitted to `StateFlow`s are **never eagerly recycled** immediately after - `minSdk = 35` (Android 15) — intentional for personal use. Lower to 26-29 if distributing. - Accompanist Permissions (`0.36.0`) is deprecated; should migrate to first-party `activity-compose` API. -- No user-facing toggle to disable GPS tagging — location is embedded whenever permission is granted. - Dependencies are pinned to late-2024 versions; periodic bumps recommended. - Fragment shader uses `int` uniform branching in GLSL ES 1.00 — works but could be cleaner with ES 3.00. diff --git a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt index 6cb7705..80ed80b 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/CameraScreen.kt @@ -39,6 +39,8 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.FlipCameraAndroid import androidx.compose.material.icons.filled.PhotoLibrary import androidx.compose.material.icons.filled.RestartAlt +import androidx.compose.material.icons.filled.LocationOff +import androidx.compose.material.icons.filled.LocationOn import androidx.compose.material.icons.filled.Tune import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon @@ -107,6 +109,7 @@ fun CameraScreen( val lastSavedUri by viewModel.lastSavedUri.collectAsState() val lastThumbnailBitmap by viewModel.lastThumbnailBitmap.collectAsState() val galleryPreviewBitmap by viewModel.galleryPreviewBitmap.collectAsState() + val geotagEnabled by viewModel.geotagEnabled.collectAsState() val isGalleryPreview = galleryPreviewBitmap != null val zoomRatio by viewModel.cameraManager.zoomRatio.collectAsState() @@ -254,6 +257,20 @@ fun CameraScreen( } Row(verticalAlignment = Alignment.CenterVertically) { + // GPS geotagging toggle + IconButton( + onClick = { + viewModel.toggleGeotag() + viewModel.haptics.tick() + } + ) { + Icon( + imageVector = if (geotagEnabled) Icons.Default.LocationOn else Icons.Default.LocationOff, + contentDescription = if (geotagEnabled) "Disable GPS geotagging" else "Enable GPS geotagging", + tint = if (geotagEnabled) Color.White else Color.White.copy(alpha = 0.5f) + ) + } + if (!isGalleryPreview) { // Camera flip button IconButton( diff --git a/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt b/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt index 9595e21..cb4d9da 100644 --- a/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt +++ b/app/src/main/java/no/naiv/tiltshift/ui/CameraViewModel.kt @@ -1,6 +1,7 @@ package no.naiv.tiltshift.ui import android.app.Application +import android.content.Context import android.graphics.Bitmap import android.location.Location import android.net.Uri @@ -40,12 +41,16 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) companion object { private const val TAG = "CameraViewModel" + private const val PREFS_NAME = "tiltshift_prefs" + private const val KEY_GEOTAG_ENABLED = "geotag_enabled" /** Max dimension for the preview source bitmap to keep effect computation fast. */ private const val PREVIEW_MAX_DIMENSION = 1024 /** Debounce delay before recomputing preview to reduce GC pressure during slider drags. */ private const val PREVIEW_DEBOUNCE_MS = 80L } + private val prefs = application.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val cameraManager = CameraManager(application) val photoSaver = PhotoSaver(application) val captureHandler = ImageCaptureHandler(application, photoSaver) @@ -98,6 +103,20 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) val isGalleryPreview: Boolean get() = _galleryBitmap.value != null + // GPS geotagging toggle (persisted) + private val _geotagEnabled = MutableStateFlow(prefs.getBoolean(KEY_GEOTAG_ENABLED, true)) + val geotagEnabled: StateFlow = _geotagEnabled.asStateFlow() + + fun toggleGeotag() { + val newValue = !_geotagEnabled.value + _geotagEnabled.value = newValue + prefs.edit().putBoolean(KEY_GEOTAG_ENABLED, newValue).apply() + } + + /** Returns current location only if geotagging is enabled. */ + private val effectiveLocation: Location? + get() = if (_geotagEnabled.value) _currentLocation.value else null + // Device state private val _currentRotation = MutableStateFlow(Surface.ROTATION_0) val currentRotation: StateFlow = _currentRotation.asStateFlow() @@ -241,7 +260,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) val result = captureHandler.processExistingImage( imageUri = uri, blurParams = _blurParams.value, - location = _currentLocation.value + location = effectiveLocation ) handleSaveResult(result) cancelGalleryPreview() @@ -263,7 +282,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application) executor = cameraManager.getExecutor(), blurParams = _blurParams.value, deviceRotation = _currentRotation.value, - location = _currentLocation.value, + location = effectiveLocation, isFrontCamera = cameraManager.isFrontCamera.value ) handleSaveResult(result)