Compare commits

...

2 commits

Author SHA1 Message Date
2a82629461 Document orientation policy and emulator-testing pitfalls in CLAUDE.md
Capture two things future sessions need to know up front: (1) the
activity is locked to portrait and the camera image stays in the
device frame on purpose — eight releases of orientation-tracking
attempts (v1.1.6 → v1.1.13) all got reverted, and per-orientation
correctness now lives in the EXIF tag rather than the GL pipeline; (2)
the Pixel 6 emulator's default virtual scene is too symmetric to
verify rotation visually, `emu rotate` decrements rather than
increments, and there is a startup window where camera bitmaps look
black.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:06:37 +02:00
b057613bac Write the user's physical tilt into the saved photo's EXIF orientation
Capture saves the bitmap in the device's portrait frame (CameraX
rotates the sensor 90° to match the locked-portrait activity), so a
photo taken while the phone was held in landscape lands on disk as a
portrait-shaped bitmap with world-up pointing to the side. Tag the
EXIF orientation with the user's actual tilt at shutter time
(OrientationEventListener-derived deviceRotation, which up to now
was passed in but ignored), so gallery viewers rotate the photo to
match how the phone was held.

Bump to 1.1.15.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:59:28 +02:00
3 changed files with 28 additions and 3 deletions

View file

@ -86,6 +86,21 @@ Bitmaps emitted to `StateFlow`s are **never eagerly recycled** immediately after
- Error/success dismiss indicators use cancellable `Job` tracking to prevent race conditions
- `writeExifToUri()` returns boolean and logs at ERROR level on failure
### Orientation policy (do not change without asking)
- Activity is `screenOrientation="portrait"` in the manifest. The GL surface and Compose layout therefore never rotate.
- The camera passthrough uses a fixed 90° texcoord rotation (`texCoordsBack`/`texCoordsFront`). There is no `setTargetRotation`/`setDisplayRotation`/`getTransformMatrix`-based rotation tracking — past attempts (v1.1.6 through v1.1.13) all introduced subtle bugs and were reverted.
- The camera image lives in the device's portrait frame and visually follows the phone as it tilts. Per-orientation correctness comes from the EXIF orientation tag written at capture (`OrientationDetector.degreesToExifOrientation(rotationToDegrees(deviceRotation), isFrontCamera)`), not from rotating the bitmap.
- `OrientationEventListener` (`viewModel.currentRotation`) is for EXIF only. It is **not** the same as `Display.rotation`: it fires at the 45° tilt threshold while the activity rotates later, so it must not drive anything that has to stay in sync with the GL surface.
- `SurfaceTexture.getTransformMatrix()` with a custom `SurfaceProvider` does not change on `Preview.targetRotation` updates; rebinding doesn't reliably help either. Don't go down that road again.
### Local Android testing
- AVD: `tilfluktsrom` (Pixel 6, API 35, Google APIs) at `~/.android/avd/`. Boot with `emulator -avd tilfluktsrom -no-snapshot-save -gpu swiftshader_indirect -no-audio`.
- `adb -s emulator-5554 emu rotate` cycles `ROTATION_0 → 3 → 2 → 1 → 0` (decrements by 90°), not the other way.
- The default virtual scene is too rotationally symmetric to verify which way is "up". For orientation testing, pass `-virtualscene-poster wall=/path/to/marker.png` and ensure the scene camera faces the wall, or just don't trust emulator screenshots as proof of correct orientation.
- Camera bitmaps captured at startup tend to look black for a few seconds while CameraX rebinds. Wait ≥10 s after `am start` before screenshotting.
## Permissions
| Permission | Purpose | Notes |

View file

@ -24,6 +24,7 @@ import kotlin.coroutines.resume
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import no.naiv.tiltshift.util.OrientationDetector
/**
* Handles capturing photos with the tilt-shift effect applied.
@ -121,10 +122,19 @@ class ImageCaptureHandler(
var thumbnail: Bitmap? = null
try {
thumbnail = createThumbnail(captureResult.processed)
// Camera bitmap is in the device's portrait frame (CameraX
// rotated the sensor 90° because the activity is locked to
// portrait). Tag the EXIF with the user's physical tilt
// when they pressed the shutter so viewers display the
// photo right-side-up regardless of how the phone was held.
val exifOrientation = OrientationDetector.degreesToExifOrientation(
OrientationDetector.rotationToDegrees(deviceRotation),
isFrontCamera
)
val result = photoSaver.saveBitmapPair(
original = captureResult.original,
processed = captureResult.processed,
orientation = ExifInterface.ORIENTATION_NORMAL,
orientation = exifOrientation,
location = location
)
if (result is SaveResult.Success) {

View file

@ -1,4 +1,4 @@
versionMajor=1
versionMinor=1
versionPatch=14
versionCode=16
versionPatch=15
versionCode=17