Commit graph

42 commits

Author SHA1 Message Date
e9bef0607f 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 <noreply@anthropic.com>
2026-03-05 13:50:33 +01:00
11a79076bc 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
12051b2a83 Add real-time tilt-shift preview for gallery images
Gallery imports now show the effect live as you adjust parameters,
matching the camera preview experience. Uses a downscaled (1024px)
source bitmap for fast recomputation via collectLatest, which
cancels stale frames when params change mid-computation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:51:26 +01:00
dedf445cf6 Skip saving original when processing gallery images
Gallery imports already have the original on-device, so only save
the tilt-shift processed version. Camera captures continue saving
both versions since the original only exists in memory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:40:06 +01:00
efe406f0b0 Refactor CameraScreen to use ViewModel with full UI improvements
Migrate all UI state from local remember{} to CameraViewModel for
surviving configuration changes. Add processing overlay indicator,
accessibility semantics on interactive elements, gallery preview
with Cancel/Apply flow, and consistent bottom bar layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:23:43 +01:00
ee2b11941a Handle permanently denied camera permission with Settings redirect
Detect when camera permission is permanently denied (not granted and
rationale not shown) and offer an "Open Settings" button instead of
a non-functional "Grant" button. Use centralized AppColors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:23:34 +01:00
527c8fd0bb Add accessibility semantics and touch targets to zoom and lens controls
Increase ZoomButton to 48dp minimum touch target, add Role.Button and
contentDescription/selected semantics, and use centralized AppColors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:15:26 +01:00
04b61fdd9e Move capture callbacks to background executor
Replace ContextCompat.getMainExecutor with a dedicated single-thread
executor for image capture callbacks. This prevents bitmap decode,
rotation, and tilt-shift effect processing from blocking the UI thread
and causing jank or ANR on slower devices.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:08:00 +01:00
72156f4e5d Add dark outlines to overlay guides and improve gesture zones
- Draw all guide lines with a dark outline behind the accent color
  to ensure visibility over bright/amber camera scenes
- Clamp gesture zone focus size to MIN_FOCUS_SIZE_PX (150px) so
  rotation zone remains usable at small focus sizes
- Add semantics contentDescription to Canvas for TalkBack
- Use AppColors for centralized color references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:07:50 +01:00
6a1d66bd4b Add ViewModel for state preservation and centralize color constants
- Create CameraViewModel with AndroidViewModel to survive configuration
  changes (rotation). All blur params, capture state, gallery preview,
  and thumbnail state now live in StateFlow fields
- Create AppColors object to centralize the 12+ hardcoded color literals
  into a single source of truth
- Add lifecycle-viewmodel-compose dependency

Fixes: state lost on rotation, orphaned capture coroutines on config
change, accent color maintenance risk

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 12:07:39 +01:00
5e08fb9c13 Fix bitmap recycle race condition and startActivity crash
- Null the Compose state reference before recycling bitmaps to prevent
  the renderer from drawing a recycled bitmap between recycle() and
  the state update
- Wrap ACTION_VIEW startActivity in try-catch for devices without
  an image viewer installed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:56:29 +01:00
983bca3600 Type-safe capture pipeline and image dimension bounds
- Replace unsafe as Any / as SaveResult casts with a sealed
  CaptureOutcome class for type-safe continuation handling
- Catch SecurityException separately with permission-specific messages
- Replace raw e.message with generic user-friendly error strings
- Add inJustDecodeBounds pre-check in loadBitmapFromUri to downsample
  images exceeding 4096px, preventing OOM from huge gallery images

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:56:20 +01:00
cc133072fc Security-harden PhotoSaver
- Catch SecurityException separately for storage permission revocation
- Replace raw e.message with generic user-friendly error strings
- Replace thread-unsafe SimpleDateFormat with java.time.DateTimeFormatter
  to prevent filename collisions under concurrent saves on Dispatchers.IO
- Remove deprecated MediaStore.Images.Media.DATA column query and the
  path field from SaveResult.Success (unreliable on scoped storage)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:56:13 +01:00
18a4289b96 Harden CameraManager error handling
- Catch SecurityException separately with permission-specific message
- Replace raw e.message with generic user-friendly error strings
- Wrap cameraProviderFuture.get() in try-catch to handle CameraX
  initialization failures instead of crashing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:56:03 +01:00
cd51c1a843 Log SecurityException in LocationProvider instead of swallowing silently
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>
2026-03-05 11:55:57 +01:00
4950feb751 Make HapticFeedback null-safe for devices without vibrator
Use safe cast (as? VibratorManager) and wrap vibrate calls in
try-catch to prevent crashes on emulators, custom ROMs, or
devices without vibration hardware.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:55:50 +01:00
0e9adebe78 Harden build and config security
- Uncomment *.jks and *.keystore in .gitignore to prevent
  accidental keystore commits
- Disable android:allowBackup to prevent ADB data extraction
- Add distributionSha256Sum to gradle-wrapper.properties for
  tamper detection of Gradle distributions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:55:24 +01:00
5d80dcfcbe Add interactive gallery preview before applying tilt-shift effect
Instead of immediately processing gallery images, show a preview where
users can adjust blur parameters before committing. Adds Cancel/Apply
buttons and hides camera-only controls during gallery preview mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:46:05 +01:00
780a8ab167 Add bottom bar gesture exclusion, dual image save, thumbnail preview, and gallery picker
- Increase bottom padding and add systemGestureExclusion to prevent
  accidental navigation gestures from the capture controls
- Save both original and processed images with shared timestamps
  (TILTSHIFT_* and ORIGINAL_*) via new saveBitmapPair() pipeline
- Show animated thumbnail of last captured photo at bottom-right;
  tap opens the image in the default photo viewer
- Add gallery picker button to process existing photos through the
  tilt-shift pipeline with full EXIF rotation support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 22:32:11 +01:00
7abb2ea5a0 Remove unused high-resolution capture option
The feature provided no benefit on Pixel 7 Pro — both standard and
hi-res modes produced 12MP images since CameraX's standard resolution
list doesn't include the full sensor output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:50:53 +01:00
5cba2fefc9 Add configurable high-resolution capture option
Add useHighResCapture toggle to CameraManager that switches between
CameraX default resolution and HIGHEST_AVAILABLE_STRATEGY. Default
is off to avoid OOM from processing very large bitmaps (e.g. 50MP).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:38:33 +01:00
c7fa8f16be Remove unnecessary SDK version checks
With minSdk=35 all Build.VERSION.SDK_INT checks for API levels below
35 are always true. Remove all version branching in HapticFeedback
(API 29/31 checks) and PhotoSaver (API 29 checks). Keep only the
modern API calls and drop @Suppress("DEPRECATION") annotations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:25:16 +01:00
41a95885c1 Remove dead code
- Delete ExifWriter.kt (instantiated but never called)
- Remove saveJpegFile() and unused imports from PhotoSaver
- Remove CameraFlipButton() and unused imports from LensSwitcher
- Remove companion object and unused imports from HapticFeedback
- Remove getZoomPresets() from LensController
- Update README to reflect ExifWriter removal and actual minSdk (35)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:24:17 +01:00
ef350e9fb7 Replace e.printStackTrace() with Log.w in PhotoSaver
EXIF write failures are non-critical (the photo is already saved)
but should still be visible in logcat. Use Log.w with a proper TAG
instead of printStackTrace().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:22:04 +01:00
6ed3e8e7b5 Propagate camera binding errors to UI
Add an error StateFlow to CameraManager so camera binding failures
are surfaced to the user instead of silently swallowed by
e.printStackTrace(). CameraScreen collects this flow and displays
errors using the existing red overlay UI. Added Log.e with proper
TAG for logcat visibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:21:38 +01:00
f0249fcd64 Add bitmap safety in onCaptureSuccess callback
Track the current bitmap through the decode→rotate→effect pipeline
with a nullable variable. On exception, the in-flight bitmap is
recycled in the catch block to prevent native memory leaks. Errors
are now logged with Log.e and a proper companion TAG.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:20:57 +01:00
593f2c5b1f Add bitmap safety in applyTiltShiftEffect()
Track all intermediate bitmaps with nullable variables and recycle
them in a finally block. This prevents native memory leaks when an
OOM or other exception occurs mid-processing. Variables are set to
null after recycle or handoff to the caller.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:20:16 +01:00
f53d6f0b1b Fix runBlocking deadlock in ImageCaptureHandler
runBlocking on the camera callback thread could deadlock when
saveBitmap() needed the main thread. Split capturePhoto() into two
phases: synchronous CPU work (decode/rotate/effect) inside the
suspendCancellableCoroutine callback, and suspend-safe saveBitmap()
after the continuation resumes in coroutine context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 15:19:41 +01:00
b235231390 Add release signing configuration
- Load signing credentials from local.properties (not committed)
- Keystore stored in .signing/ directory (not committed)
- Release builds are now signed and installable

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:23:00 +01:00
0cd41e9814 Fix front camera rotation mirroring in shader
Add uIsFrontCamera uniform to shader and adjust coordinate
transformations for front camera's mirrored texture coordinates:
- Position transform: (1-posY, 1-posX) instead of (posY, 1-posX)
- Angle transform: -angle - 90° instead of +angle + 90°

Applied to linearFocusDistance, radialFocusDistance, and blur
direction calculations in sampleBlurred.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:07:44 +01:00
b16dd971f2 Add front camera texture coordinate handling
- Add separate texture coordinates for front and back cameras
- Front camera uses mirrored coordinates for natural selfie view
- Add setFrontCamera() method to renderer for dynamic switching
- Update texture coord buffer on GL thread when camera changes
- CameraScreen observes isFrontCamera state to trigger updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 17:03:26 +01:00
a3ce90a2fe Fix aspect ratio correction for intermediate rotation angles
Apply screen aspect ratio correction to offset.y (screen X direction)
instead of offset.x (screen Y direction) in both linear and radial
mode distance calculations. This fixes angle distortion at non-90°
rotations in portrait mode.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 16:57:01 +01:00
a99b6f222f Fix coordinate transformation in shader for position and rotation
Transform screen coordinates to texture coordinates (90° CW rotation):
- Position: (x,y) -> (y, 1-x)
- Angle: θ -> θ + 90°

Applied in linearFocusDistance, radialFocusDistance, and sampleBlurred
to fix preview not matching UI overlay position and rotation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 16:53:34 +01:00
656385e48d Fix slider controls not updating preview
Use rememberUpdatedState in ControlPanel to prevent stale closure
capture during continuous slider drags. This ensures the latest
blur parameters are used when updating, avoiding conflicts with
concurrent gesture updates from TiltShiftOverlay.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 16:36:35 +01:00
d3ca23b71c Add radial mode, UI controls, front camera, update to API 35
- Add radial/elliptical blur mode with aspect ratio control
- Add UI sliders for blur intensity, falloff, and shape
- Add front camera support with flip button
- Update minimum SDK to API 35 (Android 15)
- Enable landscape orientation (fullSensor)
- Rename app to "Naiv Tilt Shift Camera"
- Set APK output name to naiv-tilt-shift
- Add project specification document

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:13:31 +01:00
e8a5fa4811 Fix state persistence, drag tracking, and shader rotation sync
State persistence:
- Use rememberUpdatedState to always get latest params inside pointerInput
- Capture gestureStartParams at beginning of each gesture
- All adjustments now use initial values + accumulated change

Drag tracking:
- Track initialDragCentroid at drag start
- Calculate total drag offset from initial point (not frame-by-frame)
- Drag now properly moves focus center 1:1

Shader rotation sync:
- Adjust angle by -90° in shader to compensate for portrait texture rotation
- Preview blur effect now rotates in sync with overlay UI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:10:18 +01:00
2736d42e74 Fix gesture tracking: 1:1 rotation and drag, better zone detection
Rotation:
- Use direct angle calculation between touch points (atan2)
- Track initial touch angle and effect angle separately
- Effect rotation now matches finger rotation exactly

Position drag:
- Remove all sensitivity dampening
- 1:1 mapping: finger moves 100px, effect center moves 100px

Gesture zones rebalanced:
- Rotation: only very center of focus zone (< 30% of focus height)
- Size adjustment: large area around effect (30% - 200%)
- Camera zoom: only far outside the effect (> 200%)

This prevents rotation from dominating size adjustments.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 16:02:43 +01:00
e53155e8ee Add 2D focus positioning, fix rotation tracking, sync preview with effect
2D positioning:
- Add positionX parameter to BlurParameters (was only Y before)
- Update shader with uPositionX and uPositionY uniforms
- Single-finger drag now moves focus center anywhere on screen
- Update gradient mask generation for capture

Rotation tracking:
- Remove dampening from rotation gesture (1:1 tracking)
- Rotate gesture now directly tracks finger movement
- Preview effect rotates in sync with overlay

Overlay and shader sync:
- Both now use same positionX, positionY, angle parameters
- Preview blur effect matches overlay visualization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:55:17 +01:00
e3e05af0b8 Fix image orientation, reduce sensitivity, fix overlay clipping
Image orientation:
- Actually rotate captured bitmap using ImageProxy.rotationDegrees
- Save with EXIF ORIENTATION_NORMAL (bitmap already correctly oriented)
- Handle front camera mirroring

Gesture sensitivity (halved again):
- Position drag: 0.15x (was 0.3x)
- Rotation: 0.2x (was 0.4x)
- Size pinch: 0.25x (was 0.5x)
- Zoom pinch: 0.4x (was 0.6x)

Overlay drawing:
- Use screen diagonal to calculate extended geometry
- Draw lines and rectangles that extend beyond screen bounds
- Prevents clipping when tilt-shift effect is rotated

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:46:43 +01:00
3bbf4f9d14 Fix portrait orientation and reduce gesture sensitivity
- Rotate texture coordinates 90° for portrait mode display
  (camera sensors are landscape-oriented by default)
- Add sensitivity dampening constants for all gesture types:
  - Position drag: 0.3x
  - Rotation: 0.4x
  - Size pinch: 0.5x
  - Zoom pinch: 0.6x
- Track accumulated drag for smoother position changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:32:31 +01:00
cc367bc713 Fix GLSL shader for OpenGL ES 2.0 compatibility
- Move #extension directive to first line (required by GLSL)
- Replace array initializer syntax with getWeight() function
  (float[]() constructor not supported in GLSL ES 1.00)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:27:55 +01:00
07e10ac9c3 Initial implementation of Tilt-Shift Camera Android app
A dedicated camera app for tilt-shift photography with:
- Real-time OpenGL ES 2.0 shader-based blur preview
- Touch gesture controls (drag, rotate, pinch) for adjusting effect
- CameraX integration for camera preview and high-res capture
- EXIF metadata with GPS location support
- MediaStore integration for saving to gallery
- Jetpack Compose UI with haptic feedback

Tech stack: Kotlin, CameraX, OpenGL ES 2.0, Jetpack Compose
Min SDK: 26 (Android 8.0), Target SDK: 35 (Android 15)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 15:26:41 +01:00