Skip to content

Conversation

@YescoSoftware
Copy link
Contributor

@YescoSoftware YescoSoftware commented Jan 9, 2026

Summary

Improves camera device enumeration on Android by properly detecting camera types (ultraWide, wideAngle, telephoto) and fixes issues with device switching and lifecycle state preservation.

Changes

Device Type Detection

  • Calculate field of view (FOV) from focal length and sensor size to accurately classify cameras
  • Classify cameras as ultraWide (FOV > 90°), telephoto (FOV < 40°), or wideAngle
  • Use actual focal lengths from camera hardware instead of hardcoded 4.25mm value
  • Works correctly on devices like Samsung that expose physical cameras as separate logical cameras

Device Switching Fixes

  • Fix currentDeviceId tracking by capturing device ID after camera binding
  • Update switchToDevice() to properly set deviceId in sessionConfig
  • Preserve isCentered flag when creating new sessionConfig in switchToDevice()
  • Fixes issue where preview would go full-screen after backgrounding/foregrounding the app after switching devices

Technical Details

  • Uses Camera2 API (CameraManager, CameraCharacteristics) for accurate device information
  • FOV calculation: fov = 2 * atan(sensorWidth / (2 * focalLength))
  • Fallback classification by focal length when sensor size unavailable
  • Each camera reports its native zoom range (1x-8x for each device)

Testing

Tested on Samsung Galaxy device with 4 cameras:

  • Camera 0: Main wide (3.98mm, ~67.5° FOV) - correctly detected as wideAngle
  • Camera 1: Front (3.59mm) - correctly detected as wideAngle
  • Camera 2: Ultra-wide (1.4mm, ~92° FOV) - correctly detected as ultraWide ✓
  • Camera 3: Front (3.59mm) - correctly detected as wideAngle

Device switching and lifecycle preservation working correctly.

Breaking Changes

None - this is backward compatible and only improves existing functionality.

Related Issues

Fixes issues with camera device detection and switching on Android devices with multiple cameras.

Summary by CodeRabbit

  • Bug Fixes

    • More reliable camera switching and flipping so the selected device is applied consistently.
    • Improved enumeration of logical and physical cameras for more accurate device lists and labels.
  • Improvements

    • Automatic lens classification (ultra-wide, wide, telephoto) for clearer device selection.
    • Better handling of preview sizing and aspect-ratio changes to reduce layout issues.
    • Expanded diagnostic logging for troubleshooting.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 9, 2026

📝 Walkthrough

Walkthrough

Records bound camera device IDs; replaces per-camera enumeration with Camera2 ID resolution and lens-type/FOV inference (logical + physical entries); switching updates SessionConfig and rebinds; flipping clears device ID and rebinds; added logging and layout/rebind adjustments.

Changes

Cohort / File(s) Summary
Camera Device Enumeration & Control
android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java
After binding, stores bound camera device ID. Rewrote device discovery to enumerate CameraX CameraInfo, resolve Camera2 logical IDs via Camera2CameraInfo, query CameraCharacteristics for focal length/FOV to infer lens types (ultraWide/wideAngle/telephoto), and produce logical and physical camera entries. switchToDevice() now updates SessionConfig (deviceId/position) and calls rebind; flipCamera() clears currentDeviceId and triggers rebind via executor. Expanded logging and preview-size/layout rebind adjustments.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CameraXView
    participant CameraProvider
    participant Camera2Info as Camera2CameraInfo
    participant Characteristics as CameraCharacteristics

    User->>CameraXView: request device list
    activate CameraXView
    CameraXView->>CameraProvider: getAvailableCameraInfos()
    CameraProvider-->>CameraXView: [CameraInfo...]
    loop each CameraInfo
        CameraXView->>Camera2Info: Camera2CameraInfo.from(cameraInfo)
        Camera2Info-->>CameraXView: cameraId (logical)
        CameraXView->>Characteristics: getCameraCharacteristics(cameraId)
        Characteristics-->>CameraXView: focalLengths, sensor info
        CameraXView->>CameraXView: infer lens type & compute FOV
        CameraXView-->>CameraXView: add logical device entry
        alt physical cameras present
            CameraXView->>Characteristics: get physical camera characteristics
            Characteristics-->>CameraXView: physical properties
            CameraXView-->>CameraXView: add physical device entry
        end
    end
    CameraXView-->>User: return device list
    deactivate CameraXView
Loading
sequenceDiagram
    participant User
    participant CameraXView
    participant SessionConfig
    participant BindProcess as bindCameraUseCases
    participant Camera2Info as Camera2CameraInfo

    User->>CameraXView: switchToDevice(deviceId)
    activate CameraXView
    CameraXView->>SessionConfig: update deviceId & position
    CameraXView->>BindProcess: call bindCameraUseCases()
    activate BindProcess
    BindProcess->>Camera2CameraInfo: resolve bound camera ID from cameraInfo
    Camera2CameraInfo-->>BindProcess: cameraId
    BindProcess-->>CameraXView: store currentDeviceId
    BindProcess-->>CameraXView: complete binding
    deactivate BindProcess
    CameraXView-->>User: device switched
    deactivate CameraXView
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through circuits, counted each eye,
Mapped logical lanes where tiny lenses lie.
I measured focal whispers, labeled each view—
I flipped, I bound, then hopped off anew. 📸

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main changes: improving Android camera device enumeration with type detection and fixing device switching. It matches the primary objectives of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java`:
- Around line 1794-1795: The code calls cameraInfo.getZoomState().getValue()
directly (used to set minZoom and maxZoom) which can be null; replace with a
null-safe check: retrieve ZoomState zoomState =
cameraInfo.getZoomState().getValue(); if zoomState is null, handle gracefully
(e.g., return/skip zoom setup or schedule an observer on
cameraInfo.getZoomState() to wait for a non-null value) and only then call
zoomState.getMinZoomRatio()/getMaxZoomRatio(); update references in CameraXView
where minZoom/maxZoom are set to avoid Objects.requireNonNull and prevent an
NPE.
🧹 Nitpick comments (1)
android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java (1)

1819-1824: Inconsistent focal length thresholds for fallback classification.

The fallback thresholds differ between logical and physical camera classification:

  • Logical (lines 1819-1823): < 2.5f → ultraWide, > 5.5f → telephoto
  • Physical (lines 1894-1895): < 3.0f → ultraWide, > 5.0f → telephoto

This inconsistency could classify the same camera differently depending on whether it's enumerated as logical or physical. Consider unifying the thresholds.

Also applies to: 1894-1896

…aXView.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java`:
- Around line 1910-1913: The code may dereference focalLengths without a null
check and uses physicalDeviceType even when focalLengths is missing; update all
accesses to focalLengths (including earlier branches that read focalLengths[0])
to first check for null and fall back to a safe default (e.g., 4.25f) before
indexing, and when creating the LensInfo instance in the physicalLenses list use
a guarded physicalDeviceType value (or a safe fallback like "unknown" or
"wideAngleFallback") when focalLengths is absent so LensInfo(...) is never
constructed with a null dereference or inconsistent device type; ensure the
checks are applied to any code paths that read focalLengths and to the
constructor call that builds new LensInfo(...).
🧹 Nitpick comments (3)
android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java (3)

786-795: LGTM! Device ID capture after binding is correctly implemented.

This properly tracks the bound camera's device ID, which is essential for device switching functionality. The duplicated code between the video-enabled and non-video branches could be extracted to reduce repetition.

♻️ Optional: Extract common device ID capture logic
                 if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
                     camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture, videoCapture);
-                    CameraInfo cameraInfo = camera.getCameraInfo();
-                    currentDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
-                    Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);
                 } else {
                     camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture);
-                    CameraInfo cameraInfo = camera.getCameraInfo();
-                    currentDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
-                    Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);
                 }
+
+                // Capture bound device ID
+                CameraInfo boundCameraInfo = camera.getCameraInfo();
+                currentDeviceId = Camera2CameraInfo.from(boundCameraInfo).getCameraId();
+                Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);

1818-1825: Inconsistent focal length fallback thresholds between logical and physical cameras.

Logical cameras use <2.5f for ultraWide and >5.5f for telephoto (lines 1820-1823), while physical cameras use <3.0f and >5.0f (lines 1895-1896). This inconsistency could cause the same physical lens to be classified differently depending on whether it's accessed as a logical or physical camera.

♻️ Suggested fix: Unify thresholds
                         } else {
                             // Fallback: classify by focal length alone
-                            if (focalLength < 2.5f) {
+                            if (focalLength < 3.0f) {
                                 deviceType = "ultraWide";
-                            } else if (focalLength > 5.5f) {
+                            } else if (focalLength > 5.0f) {
                                 deviceType = "telephoto";
                             }
                         }

Also applies to: 1895-1897


2619-2627: Inconsistent executor usage between flipCamera and switchToDevice.

flipCamera uses cameraExecutor (lines 2623-2626) while switchToDevice uses mainExecutor (line 2536). The comment on line 2622 states "Camera operations must run on main thread" but then dispatches to cameraExecutor. Since bindCameraUseCases() internally posts to mainExecutor, this works but creates an unnecessary thread hop and inconsistency.

♻️ Suggested fix: Use mainExecutor consistently
         // Clear current device ID to force position-based selection
         currentDeviceId = null;
 
-        // Camera operations must run on main thread
-        cameraExecutor.execute(() -> {
+        mainExecutor.execute(() -> {
             currentCameraSelector = buildCameraSelector();
             bindCameraUseCases();
         });

match fallback lens classification to match for virtual and physical
Fix potential null check on focalLengths null or focalLengths empty array
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java`:
- Around line 2555-2577: When reconstructing the CameraSessionConfiguration
after changing deviceId in CameraXView, the mutable targetZoom value is lost;
before creating the new CameraSessionConfiguration capture the existing
targetZoom from sessionConfig (e.g., float oldTargetZoom =
sessionConfig.getTargetZoom()) and after you reconstruct and assign
sessionConfig restore it with sessionConfig.setTargetZoom(oldTargetZoom); keep
the existing sessionConfig.setCentered(wasCentered) call and ensure you
reference CameraSessionConfiguration, sessionConfig, getTargetZoom, and
setTargetZoom in the fix.

Comment on lines +2555 to +2577
// Update sessionConfig with the new device ID
sessionConfig = new CameraSessionConfiguration(
deviceId,
position,
sessionConfig.getX(),
sessionConfig.getY(),
sessionConfig.getWidth(),
sessionConfig.getHeight(),
sessionConfig.getPaddingBottom(),
sessionConfig.getToBack(),
sessionConfig.getStoreToFile(),
sessionConfig.getEnableOpacity(),
sessionConfig.getDisableExifHeaderStripping(),
sessionConfig.getDisableAudio(),
sessionConfig.getZoomFactor(),
sessionConfig.getAspectRatio(),
sessionConfig.getGridMode(),
sessionConfig.getDisableFocusIndicator(),
sessionConfig.isVideoModeEnabled()
);

sessionConfig.setCentered(wasCentered);

Copy link
Contributor

@coderabbitai coderabbitai bot Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate and inspect CameraSessionConfiguration class
set -euo pipefail

# Find the file
echo "=== Locating CameraSessionConfiguration.java ==="
fd -a "CameraSessionConfiguration.java" || true

echo ""
echo "=== Searching for CameraSessionConfiguration definition ==="
# Search for the class definition
rg -l "class CameraSessionConfiguration" || true

Repository: Cap-go/capacitor-camera-preview

Length of output: 393


🏁 Script executed:

#!/bin/bash
# Read CameraSessionConfiguration.java to see all fields and constructor
set -euo pipefail
cat -n /home/jailuser/git/android/src/main/java/app/capgo/capacitor/camera/preview/model/CameraSessionConfiguration.java

Repository: Cap-go/capacitor-camera-preview

Length of output: 5360


🏁 Script executed:

#!/bin/bash
# Check the context around line 2555 in CameraXView.java to see how wasCentered is obtained
# and verify the full switchToDevice method
set -euo pipefail
cat -n android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java | sed -n '2530,2580p'

Repository: Cap-go/capacitor-camera-preview

Length of output: 2940


Restore targetZoom when switching devices.

The reconstruction at lines 2556–2574 creates a new CameraSessionConfiguration with all constructor parameters, but does not preserve the mutable targetZoom field (which defaults to 1.0f). While isCentered is correctly restored on line 2576, targetZoom will silently reset on every device switch. Add sessionConfig.setTargetZoom(oldTargetZoom) after line 2576, or retrieve the value before reconstruction.

🤖 Prompt for AI Agents
In `@android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java`
around lines 2555 - 2577, When reconstructing the CameraSessionConfiguration
after changing deviceId in CameraXView, the mutable targetZoom value is lost;
before creating the new CameraSessionConfiguration capture the existing
targetZoom from sessionConfig (e.g., float oldTargetZoom =
sessionConfig.getTargetZoom()) and after you reconstruct and assign
sessionConfig restore it with sessionConfig.setTargetZoom(oldTargetZoom); keep
the existing sessionConfig.setCentered(wasCentered) call and ensure you
reference CameraSessionConfiguration, sessionConfig, getTargetZoom, and
setTargetZoom in the fix.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YescoSoftware, I'll plan the code changes to preserve the targetZoom value when switching devices.

Task: Preserve targetZoom value when switching devices

The switchToDevice method needs to capture and restore the targetZoom value (similar to isCentered) when reconstructing the CameraSessionConfiguration.


🧠 Learnings used
Learnt from: Dalanir
Repo: Cap-go/capacitor-camera-preview PR: 217
File: ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift:1242-1246
Timestamp: 2025-08-29T15:37:50.891Z
Learning: In the iOS CameraController.swift handleTap method, the team wants to maintain the current behavior that forces exposureMode to .autoExpose and resets exposureTargetBias to 0.0 when tapping to focus, even though it overrides the user's current exposure mode setting.

💡 For code changes across multiple files, use @coderabbitai plan in an issue comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai ignore
this is intentional to reset back to initialZoom on device change

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@YescoSoftware, thank you for the clarification! I've noted that resetting the zoom to initialZoom on device switch is the intended behavior.

🐰


✏️ Learnings added
Learnt from: YescoSoftware
Repo: Cap-go/capacitor-camera-preview PR: 283
File: android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java:2555-2577
Timestamp: 2026-01-27T22:52:14.003Z
Learning: In the Android CameraXView.java switchToDevice method, the targetZoom field is intentionally not preserved when reconstructing CameraSessionConfiguration. The team wants zoom to reset to initialZoom when switching between camera devices, rather than maintaining the previous zoom level.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Dalanir
Repo: Cap-go/capacitor-camera-preview PR: 217
File: ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift:1242-1246
Timestamp: 2025-08-29T15:37:50.891Z
Learning: In the iOS CameraController.swift handleTap method, the team wants to maintain the current behavior that forces exposureMode to .autoExpose and resets exposureTargetBias to 0.0 when tapping to focus, even though it overrides the user's current exposure mode setting.

@riderx riderx merged commit c2d66c1 into Cap-go:main Jan 30, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants