camera: Android photo + preview + lifecycle (Phase 4a)
CameraX-backed Android implementation matching the iOS plugin's
surface (ux/camera + ux/camera/events), photo-capable only. Video
recording lands in Phase 4b (VideoCapture<Recorder>).
Modules in android/src/main/kotlin/io/swipelab/ux/camera/:
CustomLifecycleOwner drives ProcessCameraProvider's bindings
STARTED ↔ DESTROYED per instance
DeviceOrientationBridge OrientationEventListener → Surface.ROTATION_*
with 22.5° hysteresis; flutterToSurfaceRotation
+ surfaceRotationToFlutter encode/decode the
four-quadrant wire enum the iOS plugin uses
PreviewSink CameraX Preview.SurfaceProvider →
SurfaceTexture → FlutterTexture (stable
textureId across resolution renegotiations)
PhotoCapture ImageCapture wrapper, per-shot
setTargetRotation, JPEG to cache dir
CameraInstance per-controller state: lifecycle owner,
texture, ProcessCameraProvider binding,
photo + preview use-cases, lens swap
CameraPlugin channel + permission flow: camera always,
mic optional (matches iOS' "camera denial
is the only hard failure" model)
UxPlugin.kt registers CameraPlugin alongside the other plugins.
Channel parity with iOS:
availableCameras, create, initialize, dispose, setDescription,
setFlashMode, lockCaptureOrientation/unlock (no-op; preview is
pinned portrait), takePicture, audioPermissionStatus, openSettings.
startVideoRecording / stopVideoRecording return `unsupported_format`
until Phase 4b. Camera-device contention via lensesInUse + audio
claim via audioInUse mirror iOS's tracking, including the
setDescription swap (remove old lens / insert new) that closed the
device_busy leak on iOS.
Android APK builds clean against compileSdk 34, CameraX 1.3.4.