@capgo/camera-preview 8.2.1 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -96,11 +96,14 @@ import java.util.Collections;
96
96
  import java.util.Date;
97
97
  import java.util.List;
98
98
  import java.util.Locale;
99
+ import java.util.Map;
99
100
  import java.util.Objects;
100
101
  import java.util.Set;
102
+ import java.util.concurrent.ConcurrentHashMap;
101
103
  import java.util.concurrent.Executor;
102
104
  import java.util.concurrent.ExecutorService;
103
105
  import java.util.concurrent.Executors;
106
+ import java.util.concurrent.RejectedExecutionException;
104
107
  import org.json.JSONObject;
105
108
 
106
109
  public class CameraXView implements LifecycleOwner, LifecycleObserver {
@@ -138,6 +141,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
138
141
  private long focusIndicatorAnimationId = 0; // Incrementing token to invalidate previous animations
139
142
  private CameraSelector currentCameraSelector;
140
143
  private String currentDeviceId;
144
+ private String currentPhysicalDeviceId;
145
+ private String currentLogicalDeviceId;
141
146
  private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
142
147
  private CameraSessionConfiguration sessionConfig;
143
148
  private CameraXViewListener listener;
@@ -150,6 +155,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
150
155
  private final LifecycleRegistry lifecycleRegistry;
151
156
  private final Executor mainExecutor;
152
157
  private ExecutorService cameraExecutor;
158
+ private static volatile Map<String, app.capgo.capacitor.camera.preview.model.CameraDevice> enumeratedDeviceCache =
159
+ new ConcurrentHashMap<>();
160
+ private static final Object enumeratedDeviceCacheLock = new Object();
161
+ private static volatile boolean enumeratedDeviceCacheRefreshInProgress = false;
153
162
  private boolean isRunning = false;
154
163
  private Size currentPreviewResolution = null;
155
164
  private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
@@ -213,6 +222,56 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
213
222
  }
214
223
  };
215
224
 
225
+ private static final class PhysicalCameraBindingTarget {
226
+
227
+ private final CameraInfo logicalCameraInfo;
228
+ private final String logicalCameraId;
229
+ private final int requiredFacing;
230
+
231
+ private PhysicalCameraBindingTarget(CameraInfo logicalCameraInfo, String logicalCameraId, int requiredFacing) {
232
+ this.logicalCameraInfo = logicalCameraInfo;
233
+ this.logicalCameraId = logicalCameraId;
234
+ this.requiredFacing = requiredFacing;
235
+ }
236
+ }
237
+
238
+ private static final class PhysicalDeviceMetadata {
239
+
240
+ private final String position;
241
+ private final float fallbackZoom;
242
+
243
+ private PhysicalDeviceMetadata(String position, float fallbackZoom) {
244
+ this.position = position;
245
+ this.fallbackZoom = fallbackZoom;
246
+ }
247
+ }
248
+
249
+ private static final class CameraBindingPlan {
250
+
251
+ private final CameraSelector selector;
252
+ private final String reportedDeviceId;
253
+ private final String logicalCameraId;
254
+ private final String physicalCameraId;
255
+ private final float fallbackZoom;
256
+ private final boolean usesPhysicalSelection;
257
+
258
+ private CameraBindingPlan(
259
+ CameraSelector selector,
260
+ String reportedDeviceId,
261
+ String logicalCameraId,
262
+ String physicalCameraId,
263
+ float fallbackZoom,
264
+ boolean usesPhysicalSelection
265
+ ) {
266
+ this.selector = selector;
267
+ this.reportedDeviceId = reportedDeviceId;
268
+ this.logicalCameraId = logicalCameraId;
269
+ this.physicalCameraId = physicalCameraId;
270
+ this.fallbackZoom = fallbackZoom;
271
+ this.usesPhysicalSelection = usesPhysicalSelection;
272
+ }
273
+ }
274
+
216
275
  private boolean IsOperationRunning(String name) {
217
276
  synchronized (operationLock) {
218
277
  if (stopPending) {
@@ -268,7 +327,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
268
327
  this.lifecycleRegistry = new LifecycleRegistry(this);
269
328
  this.mainExecutor = ContextCompat.getMainExecutor(context);
270
329
 
271
- mainExecutor.execute(() -> lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED));
330
+ mainExecutor.execute(() -> {
331
+ if (lifecycleRegistry.getCurrentState() != Lifecycle.State.DESTROYED) {
332
+ lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
333
+ }
334
+ });
272
335
  }
273
336
 
274
337
  @NonNull
@@ -390,36 +453,92 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
390
453
  }
391
454
 
392
455
  public void startSession(CameraSessionConfiguration config) {
393
- this.sessionConfig = config;
394
- cameraExecutor = Executors.newSingleThreadExecutor();
395
-
396
- // Reset cached orientation so we don't reuse stale values across sessions
397
- synchronized (accelerometerLock) {
398
- lastAccelerometerValues[0] = 0f;
399
- lastAccelerometerValues[1] = 0f;
400
- lastAccelerometerValues[2] = 0f;
401
- }
402
- lastCaptureRotation = -1;
403
-
404
- // Start accelerometer for orientation detection regardless of lock
405
- if (sensorManager == null) {
406
- sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
407
- accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
408
- rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
409
- }
410
- if (accelerometer != null) {
411
- sensorManager.registerListener(accelerometerListener, accelerometer, SensorManager.SENSOR_DELAY_UI);
412
- }
413
- if (rotationVectorSensor != null) {
414
- sensorManager.registerListener(rotationVectorListener, rotationVectorSensor, SensorManager.SENSOR_DELAY_NORMAL);
415
- }
416
- lastCompassHeading = -1f;
417
- synchronized (operationLock) {
418
- activeOperations = 0;
419
- stopPending = false;
420
- }
421
456
  mainExecutor.execute(() -> {
457
+ // Stop may run first (e.g. activity pause) and move the registry to DESTROYED while this
458
+ // runnable is still queued — never transition backward from DESTROYED.
459
+ if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
460
+ if (listener != null) {
461
+ listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
462
+ }
463
+ return;
464
+ }
465
+ if (stopRequested) {
466
+ if (listener != null) {
467
+ listener.onCameraStartError("Camera start aborted: stop requested");
468
+ }
469
+ return;
470
+ }
471
+ Lifecycle.State state = lifecycleRegistry.getCurrentState();
472
+ if (state == Lifecycle.State.INITIALIZED) {
473
+ lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
474
+ if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
475
+ if (listener != null) {
476
+ listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
477
+ }
478
+ return;
479
+ }
480
+ if (stopRequested) {
481
+ if (listener != null) {
482
+ listener.onCameraStartError("Camera start aborted: stop requested");
483
+ }
484
+ return;
485
+ }
486
+ }
487
+ if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
488
+ if (listener != null) {
489
+ listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
490
+ }
491
+ return;
492
+ }
493
+ if (stopRequested) {
494
+ if (listener != null) {
495
+ listener.onCameraStartError("Camera start aborted: stop requested");
496
+ }
497
+ return;
498
+ }
422
499
  lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
500
+ if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
501
+ if (listener != null) {
502
+ listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
503
+ }
504
+ return;
505
+ }
506
+ if (stopRequested) {
507
+ if (listener != null) {
508
+ listener.onCameraStartError("Camera start aborted: stop requested");
509
+ }
510
+ return;
511
+ }
512
+
513
+ this.sessionConfig = config;
514
+ cameraExecutor = Executors.newSingleThreadExecutor();
515
+ requestEnumeratedDeviceCacheRefresh();
516
+
517
+ // Reset cached orientation so we don't reuse stale values across sessions
518
+ synchronized (accelerometerLock) {
519
+ lastAccelerometerValues[0] = 0f;
520
+ lastAccelerometerValues[1] = 0f;
521
+ lastAccelerometerValues[2] = 0f;
522
+ }
523
+ lastCaptureRotation = -1;
524
+
525
+ // Start accelerometer for orientation detection regardless of lock
526
+ if (sensorManager == null) {
527
+ sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
528
+ accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
529
+ rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
530
+ }
531
+ if (accelerometer != null) {
532
+ sensorManager.registerListener(accelerometerListener, accelerometer, SensorManager.SENSOR_DELAY_UI);
533
+ }
534
+ if (rotationVectorSensor != null) {
535
+ sensorManager.registerListener(rotationVectorListener, rotationVectorSensor, SensorManager.SENSOR_DELAY_NORMAL);
536
+ }
537
+ lastCompassHeading = -1f;
538
+ synchronized (operationLock) {
539
+ activeOperations = 0;
540
+ stopPending = false;
541
+ }
423
542
  setupCamera();
424
543
  });
425
544
  }
@@ -464,6 +583,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
464
583
 
465
584
  private void performImmediateStop() {
466
585
  isRunning = false;
586
+ currentDeviceId = null;
587
+ currentPhysicalDeviceId = null;
588
+ currentLogicalDeviceId = null;
467
589
  // Stop accelerometer and rotation vector sensor
468
590
  if (sensorManager != null && accelerometer != null) {
469
591
  sensorManager.unregisterListener(accelerometerListener);
@@ -483,7 +605,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
483
605
  if (cameraProvider != null) {
484
606
  cameraProvider.unbindAll();
485
607
  }
486
- lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
487
608
  if (cameraExecutor != null) {
488
609
  cameraExecutor.shutdown();
489
610
  }
@@ -527,7 +648,31 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
527
648
  cameraProviderFuture.addListener(
528
649
  () -> {
529
650
  try {
651
+ if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
652
+ if (listener != null) {
653
+ listener.onCameraStartError("Camera binding cancelled: lifecycle destroyed (before provider)");
654
+ }
655
+ return;
656
+ }
657
+ if (stopRequested) {
658
+ if (listener != null) {
659
+ listener.onCameraStartError("Camera binding cancelled: stop requested (before provider)");
660
+ }
661
+ return;
662
+ }
530
663
  cameraProvider = cameraProviderFuture.get();
664
+ if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
665
+ if (listener != null) {
666
+ listener.onCameraStartError("Camera binding cancelled: lifecycle destroyed (after provider)");
667
+ }
668
+ return;
669
+ }
670
+ if (stopRequested) {
671
+ if (listener != null) {
672
+ listener.onCameraStartError("Camera binding cancelled: stop requested (after provider)");
673
+ }
674
+ return;
675
+ }
531
676
  setupPreviewView();
532
677
  bindCameraUseCases();
533
678
  } catch (Exception e) {
@@ -662,6 +807,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
662
807
  int width = sessionConfig.getWidth();
663
808
  int height = sessionConfig.getHeight();
664
809
  String aspectRatio = sessionConfig.getAspectRatio();
810
+ String aspectMode = sessionConfig.getAspectMode();
665
811
 
666
812
  // Get comprehensive display information
667
813
  int screenWidthPx, screenHeightPx;
@@ -731,7 +877,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
731
877
  );
732
878
 
733
879
  // Apply aspect ratio if specified
734
- if (aspectRatio != null && !aspectRatio.isEmpty() && sessionConfig.isCentered()) {
880
+ if (aspectRatio != null && !aspectRatio.isEmpty() && sessionConfig.isCentered() && !"cover".equals(aspectMode)) {
735
881
  String[] ratios = aspectRatio.split(":");
736
882
  if (ratios.length == 2) {
737
883
  try {
@@ -846,7 +992,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
846
992
  " and position: " +
847
993
  sessionConfig.getPosition()
848
994
  );
849
- currentCameraSelector = buildCameraSelector();
995
+ CameraBindingPlan bindingPlan = buildCameraBindingPlan(sessionConfig);
996
+ currentCameraSelector = bindingPlan.selector;
850
997
 
851
998
  ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder().setResolutionStrategy(
852
999
  ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
@@ -924,17 +1071,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
924
1071
  // is connected and video frames are captured correctly
925
1072
  preview.setSurfaceProvider(previewView.getSurfaceProvider());
926
1073
 
927
- // Bind with or without video capture based on enableVideoMode
928
- if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
929
- camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture, videoCapture);
930
- CameraInfo cameraInfo = camera.getCameraInfo();
931
- currentDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
932
- Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);
933
- } else {
934
- camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture);
935
- CameraInfo cameraInfo = camera.getCameraInfo();
936
- currentDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
937
- Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);
1074
+ try {
1075
+ bindConfiguredUseCases(bindingPlan, preview);
1076
+ } catch (Exception initialBindError) {
1077
+ if (!bindingPlan.usesPhysicalSelection) {
1078
+ throw initialBindError;
1079
+ }
1080
+
1081
+ Log.w(
1082
+ TAG,
1083
+ "bindCameraUseCases: Physical camera binding failed for " +
1084
+ bindingPlan.physicalCameraId +
1085
+ ", falling back to logical camera behavior",
1086
+ initialBindError
1087
+ );
1088
+
1089
+ bindingPlan = buildLogicalFallbackPlan(sessionConfig, bindingPlan);
1090
+ currentCameraSelector = bindingPlan.selector;
1091
+ bindConfiguredUseCases(bindingPlan, preview);
938
1092
  }
939
1093
 
940
1094
  resetExposureCompensationToDefault();
@@ -942,7 +1096,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
942
1096
  // Log details about the active camera
943
1097
  Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
944
1098
  CameraInfo cameraInfo = camera.getCameraInfo();
945
- Log.d(TAG, "Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId());
1099
+ Log.d(TAG, "Bound Camera ID: " + currentLogicalDeviceId);
1100
+ if (currentPhysicalDeviceId != null) {
1101
+ Log.d(TAG, "Bound Physical Camera ID: " + currentPhysicalDeviceId);
1102
+ }
946
1103
 
947
1104
  // Log zoom state
948
1105
  ZoomState zoomState = cameraInfo.getZoomState().getValue();
@@ -1018,7 +1175,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1018
1175
  previewView.setScaleType("cover".equals(aspectMode) ? PreviewView.ScaleType.FILL_CENTER : PreviewView.ScaleType.FIT_CENTER);
1019
1176
 
1020
1177
  // Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
1021
- float initialZoom = sessionConfig.getTargetZoom() != 1.0f ? sessionConfig.getTargetZoom() : sessionConfig.getZoomFactor();
1178
+ float initialZoom = !bindingPlan.usesPhysicalSelection &&
1179
+ bindingPlan.fallbackZoom != 1.0f &&
1180
+ sessionConfig.getTargetZoom() == 1.0f
1181
+ ? bindingPlan.fallbackZoom
1182
+ : (sessionConfig.getTargetZoom() != 1.0f ? sessionConfig.getTargetZoom() : sessionConfig.getZoomFactor());
1022
1183
  if (initialZoom != 1.0f) {
1023
1184
  Log.d(TAG, "Applying initial zoom of " + initialZoom);
1024
1185
 
@@ -1088,24 +1249,320 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1088
1249
 
1089
1250
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
1090
1251
  private CameraSelector buildCameraSelector() {
1091
- CameraSelector.Builder builder = new CameraSelector.Builder();
1092
- final String deviceId = sessionConfig.getDeviceId();
1252
+ return buildCameraBindingPlan(sessionConfig).selector;
1253
+ }
1254
+
1255
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
1256
+ private CameraBindingPlan buildCameraBindingPlan(CameraSessionConfiguration config) {
1257
+ final String deviceId = config.getDeviceId();
1258
+ final String position = config.getPosition();
1093
1259
 
1094
1260
  if (deviceId != null && !deviceId.isEmpty()) {
1095
- builder.addCameraFilter((cameraInfos) -> {
1096
- for (CameraInfo cameraInfo : cameraInfos) {
1097
- if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1098
- return Collections.singletonList(cameraInfo);
1261
+ CameraInfo directCameraInfo = findAvailableCameraInfoById(deviceId);
1262
+ if (directCameraInfo != null) {
1263
+ CameraSelector.Builder directBuilder = new CameraSelector.Builder();
1264
+ directBuilder.addCameraFilter((cameraInfos) -> {
1265
+ for (CameraInfo cameraInfo : cameraInfos) {
1266
+ if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1267
+ return Collections.singletonList(cameraInfo);
1268
+ }
1099
1269
  }
1270
+ return Collections.emptyList();
1271
+ });
1272
+
1273
+ return new CameraBindingPlan(directBuilder.build(), deviceId, deviceId, null, 1.0f, false);
1274
+ }
1275
+
1276
+ CameraBindingPlan logicalFallbackPlan = buildLogicalFallbackPlanForDeviceId(deviceId);
1277
+ if (config.isPhysicalDeviceSelectionEnabled()) {
1278
+ PhysicalCameraBindingTarget physicalTarget = findPhysicalCameraBindingTarget(deviceId);
1279
+ if (physicalTarget != null) {
1280
+ CameraSelector.Builder physicalBuilder = new CameraSelector.Builder()
1281
+ .requireLensFacing(physicalTarget.requiredFacing)
1282
+ .setPhysicalCameraId(deviceId)
1283
+ .addCameraFilter((cameraInfos) -> {
1284
+ for (CameraInfo cameraInfo : cameraInfos) {
1285
+ if (physicalTarget.logicalCameraId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1286
+ return Collections.singletonList(cameraInfo);
1287
+ }
1288
+ }
1289
+ return Collections.emptyList();
1290
+ });
1291
+
1292
+ return new CameraBindingPlan(
1293
+ physicalBuilder.build(),
1294
+ deviceId,
1295
+ physicalTarget.logicalCameraId,
1296
+ deviceId,
1297
+ logicalFallbackPlan != null ? logicalFallbackPlan.fallbackZoom : getFallbackZoomForDeviceId(deviceId),
1298
+ true
1299
+ );
1100
1300
  }
1101
- return Collections.emptyList();
1102
- });
1301
+ }
1302
+
1303
+ if (logicalFallbackPlan != null) {
1304
+ return logicalFallbackPlan;
1305
+ }
1306
+
1307
+ throw invalidDeviceId(deviceId);
1308
+ }
1309
+
1310
+ return buildPositionPlan(position);
1311
+ }
1312
+
1313
+ private CameraBindingPlan buildLogicalFallbackPlan(CameraSessionConfiguration config, CameraBindingPlan failedPhysicalPlan) {
1314
+ String fallbackPosition = config.getPosition();
1315
+
1316
+ if (failedPhysicalPlan.logicalCameraId != null) {
1317
+ CameraInfo logicalCameraInfo = findAvailableCameraInfoById(failedPhysicalPlan.logicalCameraId);
1318
+ if (logicalCameraInfo != null) {
1319
+ fallbackPosition = isBackCamera(logicalCameraInfo) ? "rear" : "front";
1320
+ }
1321
+ }
1322
+
1323
+ CameraBindingPlan positionPlan = buildPositionPlan(fallbackPosition);
1324
+ return new CameraBindingPlan(
1325
+ positionPlan.selector,
1326
+ failedPhysicalPlan.reportedDeviceId,
1327
+ positionPlan.logicalCameraId,
1328
+ null,
1329
+ failedPhysicalPlan.fallbackZoom,
1330
+ false
1331
+ );
1332
+ }
1333
+
1334
+ private CameraBindingPlan buildLogicalFallbackPlanForDeviceId(String deviceId) {
1335
+ String fallbackPosition = resolveFallbackPositionForDeviceId(deviceId);
1336
+ if (fallbackPosition == null) {
1337
+ return null;
1338
+ }
1339
+
1340
+ CameraBindingPlan positionPlan = buildPositionPlan(fallbackPosition);
1341
+ return new CameraBindingPlan(
1342
+ positionPlan.selector,
1343
+ deviceId,
1344
+ positionPlan.logicalCameraId,
1345
+ null,
1346
+ getFallbackZoomForDeviceId(deviceId),
1347
+ false
1348
+ );
1349
+ }
1350
+
1351
+ private CameraBindingPlan buildPositionPlan(String position) {
1352
+ int requiredFacing = "front".equals(position) ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
1353
+ CameraSelector selector = new CameraSelector.Builder().requireLensFacing(requiredFacing).build();
1354
+ return new CameraBindingPlan(selector, null, null, null, 1.0f, false);
1355
+ }
1356
+
1357
+ private IllegalArgumentException invalidDeviceId(String deviceId) {
1358
+ return new IllegalArgumentException("Unknown or unsupported deviceId: " + deviceId);
1359
+ }
1360
+
1361
+ private CameraInfo findAvailableCameraInfoById(String deviceId) {
1362
+ if (cameraProvider == null || deviceId == null || deviceId.isEmpty()) {
1363
+ return null;
1364
+ }
1365
+
1366
+ for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
1367
+ if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1368
+ return cameraInfo;
1369
+ }
1370
+ }
1371
+
1372
+ return null;
1373
+ }
1374
+
1375
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
1376
+ private PhysicalCameraBindingTarget findPhysicalCameraBindingTarget(String physicalDeviceId) {
1377
+ if (
1378
+ cameraProvider == null ||
1379
+ physicalDeviceId == null ||
1380
+ physicalDeviceId.isEmpty() ||
1381
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1382
+ ) {
1383
+ return null;
1384
+ }
1385
+
1386
+ CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
1387
+ if (cameraManager == null) {
1388
+ return null;
1389
+ }
1390
+
1391
+ for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
1392
+ String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
1393
+ try {
1394
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(logicalCameraId);
1395
+ if (characteristics.getPhysicalCameraIds().contains(physicalDeviceId)) {
1396
+ int requiredFacing = isBackCamera(cameraInfo) ? CameraSelector.LENS_FACING_BACK : CameraSelector.LENS_FACING_FRONT;
1397
+ return new PhysicalCameraBindingTarget(cameraInfo, logicalCameraId, requiredFacing);
1398
+ }
1399
+ } catch (CameraAccessException e) {
1400
+ Log.w(TAG, "findPhysicalCameraBindingTarget: Failed to inspect logical camera " + logicalCameraId, e);
1401
+ }
1402
+ }
1403
+
1404
+ return null;
1405
+ }
1406
+
1407
+ private PhysicalDeviceMetadata resolvePhysicalDeviceMetadata(String deviceId) {
1408
+ if (deviceId == null || deviceId.isEmpty()) {
1409
+ return null;
1410
+ }
1411
+
1412
+ CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
1413
+ if (cameraManager == null) {
1414
+ return null;
1415
+ }
1416
+
1417
+ try {
1418
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceId);
1419
+ Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
1420
+ String position = lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_FRONT ? "front" : "rear";
1421
+ return new PhysicalDeviceMetadata(position, getFallbackZoomForCharacteristics(characteristics));
1422
+ } catch (CameraAccessException | IllegalArgumentException e) {
1423
+ Log.w(TAG, "resolvePhysicalDeviceMetadata: Failed to inspect camera " + deviceId, e);
1424
+ return null;
1425
+ }
1426
+ }
1427
+
1428
+ private String resolveFallbackPositionForDeviceId(String deviceId) {
1429
+ app.capgo.capacitor.camera.preview.model.CameraDevice device = findEnumeratedDeviceById(deviceId);
1430
+ if (device != null) {
1431
+ return device.getPosition();
1432
+ }
1433
+
1434
+ PhysicalDeviceMetadata metadata = resolvePhysicalDeviceMetadata(deviceId);
1435
+ return metadata != null ? metadata.position : null;
1436
+ }
1437
+
1438
+ private app.capgo.capacitor.camera.preview.model.CameraDevice findEnumeratedDeviceById(String deviceId) {
1439
+ if (deviceId == null || deviceId.isEmpty()) {
1440
+ return null;
1441
+ }
1442
+
1443
+ app.capgo.capacitor.camera.preview.model.CameraDevice cachedDevice = enumeratedDeviceCache.get(deviceId);
1444
+ if (cachedDevice != null) {
1445
+ return cachedDevice;
1446
+ }
1447
+
1448
+ requestEnumeratedDeviceCacheRefresh();
1449
+ return null;
1450
+ }
1451
+
1452
+ private float getFallbackZoomForDeviceId(String deviceId) {
1453
+ app.capgo.capacitor.camera.preview.model.CameraDevice device = findEnumeratedDeviceById(deviceId);
1454
+ if (device != null) {
1455
+ for (LensInfo lens : device.getLenses()) {
1456
+ if ("ultraWide".equals(lens.getDeviceType())) {
1457
+ return 0.5f;
1458
+ }
1459
+ if ("telephoto".equals(lens.getDeviceType())) {
1460
+ return 2.0f;
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ PhysicalDeviceMetadata metadata = resolvePhysicalDeviceMetadata(deviceId);
1466
+ if (metadata != null) {
1467
+ return metadata.fallbackZoom;
1468
+ }
1469
+
1470
+ return 1.0f;
1471
+ }
1472
+
1473
+ private float getFallbackZoomForCharacteristics(CameraCharacteristics characteristics) {
1474
+ float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
1475
+ android.util.SizeF sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
1476
+
1477
+ if (focalLengths != null && focalLengths.length > 0) {
1478
+ float focalLength = focalLengths[0];
1479
+ if (sensorSize != null && sensorSize.getWidth() > 0) {
1480
+ double fov = 2 * Math.toDegrees(Math.atan(sensorSize.getWidth() / (2 * focalLength)));
1481
+ if (fov > 90) {
1482
+ return 0.5f;
1483
+ }
1484
+ if (fov < 40) {
1485
+ return 2.0f;
1486
+ }
1487
+ } else {
1488
+ if (focalLength < 3.0f) {
1489
+ return 0.5f;
1490
+ }
1491
+ if (focalLength > 5.0f) {
1492
+ return 2.0f;
1493
+ }
1494
+ }
1495
+ }
1496
+
1497
+ return 1.0f;
1498
+ }
1499
+
1500
+ private void bindConfiguredUseCases(CameraBindingPlan bindingPlan, Preview preview) {
1501
+ if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
1502
+ camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, videoCapture);
1103
1503
  } else {
1104
- String position = sessionConfig.getPosition();
1105
- int requiredFacing = "front".equals(position) ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
1106
- builder.requireLensFacing(requiredFacing);
1504
+ camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture);
1505
+ }
1506
+
1507
+ CameraInfo cameraInfo = camera.getCameraInfo();
1508
+ currentLogicalDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
1509
+ currentPhysicalDeviceId = bindingPlan.physicalCameraId;
1510
+ currentDeviceId = currentPhysicalDeviceId != null ? currentPhysicalDeviceId : currentLogicalDeviceId;
1511
+
1512
+ Log.d(
1513
+ TAG,
1514
+ "bindConfiguredUseCases: Camera successfully bound. activeDeviceId=" +
1515
+ currentDeviceId +
1516
+ ", logicalCameraId=" +
1517
+ currentLogicalDeviceId +
1518
+ ", physicalCameraId=" +
1519
+ currentPhysicalDeviceId
1520
+ );
1521
+ }
1522
+
1523
+ private void copyMutableSessionConfigState(CameraSessionConfiguration source, CameraSessionConfiguration target) {
1524
+ target.setCentered(source.isCentered());
1525
+ target.setTargetZoom(source.getTargetZoom());
1526
+ target.setEnablePhysicalDeviceSelection(source.isPhysicalDeviceSelectionEnabled());
1527
+ }
1528
+
1529
+ private void requestEnumeratedDeviceCacheRefresh() {
1530
+ synchronized (enumeratedDeviceCacheLock) {
1531
+ if (enumeratedDeviceCacheRefreshInProgress) {
1532
+ return;
1533
+ }
1534
+ enumeratedDeviceCacheRefreshInProgress = true;
1535
+ }
1536
+
1537
+ Runnable refreshTask = () -> {
1538
+ try {
1539
+ getAvailableDevicesStatic(context);
1540
+ } finally {
1541
+ synchronized (enumeratedDeviceCacheLock) {
1542
+ enumeratedDeviceCacheRefreshInProgress = false;
1543
+ }
1544
+ }
1545
+ };
1546
+
1547
+ if (cameraExecutor != null && !cameraExecutor.isShutdown()) {
1548
+ try {
1549
+ cameraExecutor.execute(refreshTask);
1550
+ return;
1551
+ } catch (RejectedExecutionException e) {
1552
+ Log.w(TAG, "requestEnumeratedDeviceCacheRefresh: cameraExecutor rejected refresh task", e);
1553
+ }
1554
+ }
1555
+
1556
+ try {
1557
+ Thread refreshThread = new Thread(refreshTask, "CameraPreview-DeviceCacheRefresh");
1558
+ refreshThread.setDaemon(true);
1559
+ refreshThread.start();
1560
+ } catch (RuntimeException e) {
1561
+ synchronized (enumeratedDeviceCacheLock) {
1562
+ enumeratedDeviceCacheRefreshInProgress = false;
1563
+ }
1564
+ throw e;
1107
1565
  }
1108
- return builder.build();
1109
1566
  }
1110
1567
 
1111
1568
  private static boolean isBackCamera(androidx.camera.core.CameraInfo cameraInfo) {
@@ -2191,6 +2648,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2191
2648
  }
2192
2649
 
2193
2650
  Log.d(TAG, "=== Enumeration Complete: " + devices.size() + " cameras ===");
2651
+ updateEnumeratedDeviceCache(devices);
2194
2652
  return devices;
2195
2653
  } catch (Exception e) {
2196
2654
  Log.e(TAG, "getAvailableDevicesStatic: Error getting devices", e);
@@ -2198,6 +2656,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2198
2656
  }
2199
2657
  }
2200
2658
 
2659
+ private static void updateEnumeratedDeviceCache(List<app.capgo.capacitor.camera.preview.model.CameraDevice> devices) {
2660
+ Map<String, app.capgo.capacitor.camera.preview.model.CameraDevice> newCache = new ConcurrentHashMap<>();
2661
+ for (app.capgo.capacitor.camera.preview.model.CameraDevice device : devices) {
2662
+ newCache.put(device.getDeviceId(), device);
2663
+ }
2664
+ enumeratedDeviceCache = newCache;
2665
+ }
2666
+
2201
2667
  public static ZoomFactors getZoomFactorsStatic() {
2202
2668
  try {
2203
2669
  // For static method, return default zoom factors
@@ -2275,6 +2741,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2275
2741
  }
2276
2742
 
2277
2743
  camera.getCameraControl().setZoomRatio(zoomRatio);
2744
+ if (sessionConfig != null) {
2745
+ sessionConfig.setTargetZoom(zoomRatio);
2746
+ }
2278
2747
  // Note: autofocus is intentionally not triggered on zoom because it's done by CameraX
2279
2748
  } catch (Exception e) {
2280
2749
  Log.e(TAG, "Failed to set zoom: " + e.getMessage());
@@ -2798,53 +3267,52 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2798
3267
 
2799
3268
  mainExecutor.execute(() -> {
2800
3269
  try {
2801
- // Standard physical device selection logic...
2802
- List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
2803
-
2804
- CameraInfo targetCameraInfo = null;
2805
- for (CameraInfo cameraInfo : cameraInfos) {
2806
- String id = Camera2CameraInfo.from(cameraInfo).getCameraId();
2807
- if (deviceId.equals(id)) {
2808
- targetCameraInfo = cameraInfo;
2809
- break;
3270
+ CameraSessionConfiguration previousConfig = sessionConfig;
3271
+ CameraInfo targetCameraInfo = findAvailableCameraInfoById(deviceId);
3272
+ String fallbackPosition = resolveFallbackPositionForDeviceId(deviceId);
3273
+ String position = fallbackPosition != null ? fallbackPosition : previousConfig.getPosition();
3274
+ if (targetCameraInfo != null) {
3275
+ position = isBackCamera(targetCameraInfo) ? "rear" : "front";
3276
+ } else if (previousConfig.isPhysicalDeviceSelectionEnabled()) {
3277
+ PhysicalCameraBindingTarget physicalTarget = findPhysicalCameraBindingTarget(deviceId);
3278
+ if (physicalTarget != null) {
3279
+ position = physicalTarget.requiredFacing == CameraSelector.LENS_FACING_FRONT ? "front" : "rear";
3280
+ } else if (fallbackPosition == null) {
3281
+ Log.e(TAG, "switchToDevice: Could not resolve deviceId: " + deviceId);
3282
+ return;
2810
3283
  }
3284
+ } else if (fallbackPosition == null) {
3285
+ Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
3286
+ return;
2811
3287
  }
2812
3288
 
2813
- if (targetCameraInfo != null) {
2814
- // Determine position from the target camera
2815
- String position = isBackCamera(targetCameraInfo) ? "rear" : "front";
2816
- boolean wasCentered = sessionConfig.isCentered();
2817
-
2818
- // Update sessionConfig with the new device ID
2819
- sessionConfig = new CameraSessionConfiguration(
2820
- deviceId,
2821
- position,
2822
- sessionConfig.getX(),
2823
- sessionConfig.getY(),
2824
- sessionConfig.getWidth(),
2825
- sessionConfig.getHeight(),
2826
- sessionConfig.getPaddingBottom(),
2827
- sessionConfig.getToBack(),
2828
- sessionConfig.getStoreToFile(),
2829
- sessionConfig.getEnableOpacity(),
2830
- sessionConfig.getDisableExifHeaderStripping(),
2831
- sessionConfig.getDisableAudio(),
2832
- sessionConfig.getZoomFactor(),
2833
- sessionConfig.getAspectRatio(),
2834
- sessionConfig.getAspectMode(),
2835
- sessionConfig.getGridMode(),
2836
- sessionConfig.getDisableFocusIndicator(),
2837
- sessionConfig.isVideoModeEnabled(),
2838
- sessionConfig.getVideoQuality()
2839
- );
2840
-
2841
- sessionConfig.setCentered(wasCentered);
3289
+ CameraSessionConfiguration updatedConfig = new CameraSessionConfiguration(
3290
+ deviceId,
3291
+ position,
3292
+ previousConfig.getX(),
3293
+ previousConfig.getY(),
3294
+ previousConfig.getWidth(),
3295
+ previousConfig.getHeight(),
3296
+ previousConfig.getPaddingBottom(),
3297
+ previousConfig.getToBack(),
3298
+ previousConfig.getStoreToFile(),
3299
+ previousConfig.getEnableOpacity(),
3300
+ previousConfig.getDisableExifHeaderStripping(),
3301
+ previousConfig.getDisableAudio(),
3302
+ previousConfig.getZoomFactor(),
3303
+ previousConfig.getAspectRatio(),
3304
+ previousConfig.getAspectMode(),
3305
+ previousConfig.getGridMode(),
3306
+ previousConfig.getDisableFocusIndicator(),
3307
+ previousConfig.isVideoModeEnabled(),
3308
+ previousConfig.getVideoQuality()
3309
+ );
3310
+ copyMutableSessionConfigState(previousConfig, updatedConfig);
3311
+ updatedConfig.setTargetZoom(1.0f);
3312
+ sessionConfig = updatedConfig;
2842
3313
 
2843
- Log.d(TAG, "switchToDevice: Updated sessionConfig with deviceId: " + deviceId);
2844
- bindCameraUseCases(); // Will now use deviceId from sessionConfig
2845
- } else {
2846
- Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
2847
- }
3314
+ Log.d(TAG, "switchToDevice: Updated sessionConfig with deviceId: " + deviceId);
3315
+ bindCameraUseCases();
2848
3316
  } catch (Exception e) {
2849
3317
  Log.e(TAG, "switchToDevice: Error switching camera", e);
2850
3318
  }
@@ -2862,38 +3330,39 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2862
3330
  boolean wasCentered = sessionConfig.isCentered();
2863
3331
  Log.d(TAG, "flipCamera: Switching from " + currentPosition + " to " + newPosition);
2864
3332
 
3333
+ CameraSessionConfiguration previousConfig = sessionConfig;
2865
3334
  sessionConfig = new CameraSessionConfiguration(
2866
3335
  null, // deviceId - clear device ID to force position-based selection
2867
3336
  newPosition, // position
2868
- sessionConfig.getX(), // x
2869
- sessionConfig.getY(), // y
2870
- sessionConfig.getWidth(), // width
2871
- sessionConfig.getHeight(), // height
2872
- sessionConfig.getPaddingBottom(), // paddingBottom
2873
- sessionConfig.isToBack(), // toBack
2874
- sessionConfig.isStoreToFile(), // storeToFile
2875
- sessionConfig.isEnableOpacity(), // enableOpacity
2876
- sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
2877
- sessionConfig.isDisableAudio(), // disableAudio
2878
- sessionConfig.getZoomFactor(), // zoomFactor
2879
- sessionConfig.getAspectRatio(), // aspectRatio
2880
- sessionConfig.getAspectMode(), // aspectMode
2881
- sessionConfig.getGridMode(), // gridMode
2882
- sessionConfig.getDisableFocusIndicator(), // disableFocusIndicator
2883
- sessionConfig.isVideoModeEnabled(), // enableVideoMode
2884
- sessionConfig.getVideoQuality() // videoQuality
3337
+ previousConfig.getX(), // x
3338
+ previousConfig.getY(), // y
3339
+ previousConfig.getWidth(), // width
3340
+ previousConfig.getHeight(), // height
3341
+ previousConfig.getPaddingBottom(), // paddingBottom
3342
+ previousConfig.isToBack(), // toBack
3343
+ previousConfig.isStoreToFile(), // storeToFile
3344
+ previousConfig.isEnableOpacity(), // enableOpacity
3345
+ previousConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
3346
+ previousConfig.isDisableAudio(), // disableAudio
3347
+ previousConfig.getZoomFactor(), // zoomFactor
3348
+ previousConfig.getAspectRatio(), // aspectRatio
3349
+ previousConfig.getAspectMode(), // aspectMode
3350
+ previousConfig.getGridMode(), // gridMode
3351
+ previousConfig.getDisableFocusIndicator(), // disableFocusIndicator
3352
+ previousConfig.isVideoModeEnabled(), // enableVideoMode
3353
+ previousConfig.getVideoQuality() // videoQuality
2885
3354
  );
2886
-
3355
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3356
+ sessionConfig.setTargetZoom(1.0f);
2887
3357
  sessionConfig.setCentered(wasCentered);
2888
3358
 
2889
- // Clear current device ID to force position-based selection
3359
+ // Clear current device IDs to force position-based selection
2890
3360
  currentDeviceId = null;
3361
+ currentPhysicalDeviceId = null;
3362
+ currentLogicalDeviceId = null;
2891
3363
 
2892
- // Camera operations must run on main thread
2893
- cameraExecutor.execute(() -> {
2894
- currentCameraSelector = buildCameraSelector();
2895
- bindCameraUseCases();
2896
- });
3364
+ // Rebind camera with the new position
3365
+ bindCameraUseCases();
2897
3366
  }
2898
3367
 
2899
3368
  public void setOpacity(float opacity) {
@@ -2977,32 +3446,34 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2977
3446
  return;
2978
3447
  }
2979
3448
 
2980
- String currentGridMode = sessionConfig.getGridMode();
3449
+ CameraSessionConfiguration previousConfig = sessionConfig;
3450
+ String currentGridMode = previousConfig.getGridMode();
2981
3451
  Log.d(TAG, "Changing aspect ratio from " + currentAspectRatio + " to " + aspectRatio);
2982
3452
  Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
2983
3453
 
2984
3454
  // Match iOS behavior: when aspect ratio changes, always auto-center
2985
3455
  sessionConfig = new CameraSessionConfiguration(
2986
- sessionConfig.getDeviceId(),
2987
- sessionConfig.getPosition(),
3456
+ previousConfig.getDeviceId(),
3457
+ previousConfig.getPosition(),
2988
3458
  -1, // Force auto-center X (iOS: self.posX = -1)
2989
3459
  -1, // Force auto-center Y (iOS: self.posY = -1)
2990
- sessionConfig.getWidth(),
2991
- sessionConfig.getHeight(),
2992
- sessionConfig.getPaddingBottom(),
2993
- sessionConfig.getToBack(),
2994
- sessionConfig.getStoreToFile(),
2995
- sessionConfig.getEnableOpacity(),
2996
- sessionConfig.getDisableExifHeaderStripping(),
2997
- sessionConfig.getDisableAudio(),
2998
- sessionConfig.getZoomFactor(),
3460
+ previousConfig.getWidth(),
3461
+ previousConfig.getHeight(),
3462
+ previousConfig.getPaddingBottom(),
3463
+ previousConfig.getToBack(),
3464
+ previousConfig.getStoreToFile(),
3465
+ previousConfig.getEnableOpacity(),
3466
+ previousConfig.getDisableExifHeaderStripping(),
3467
+ previousConfig.getDisableAudio(),
3468
+ previousConfig.getZoomFactor(),
2999
3469
  aspectRatio,
3000
- sessionConfig.getAspectMode(),
3470
+ previousConfig.getAspectMode(),
3001
3471
  currentGridMode,
3002
- sessionConfig.getDisableFocusIndicator(),
3003
- sessionConfig.isVideoModeEnabled(),
3004
- sessionConfig.getVideoQuality()
3472
+ previousConfig.getDisableFocusIndicator(),
3473
+ previousConfig.isVideoModeEnabled(),
3474
+ previousConfig.getVideoQuality()
3005
3475
  );
3476
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3006
3477
  sessionConfig.setCentered(true);
3007
3478
 
3008
3479
  // Update layout and rebind camera with new aspect ratio
@@ -3053,32 +3524,34 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3053
3524
  return;
3054
3525
  }
3055
3526
 
3056
- String currentGridMode = sessionConfig.getGridMode();
3527
+ CameraSessionConfiguration previousConfig = sessionConfig;
3528
+ String currentGridMode = previousConfig.getGridMode();
3057
3529
  Log.d(TAG, "Forcing aspect ratio recalculation for: " + aspectRatio);
3058
3530
  Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
3059
3531
 
3060
3532
  // Match iOS behavior: when aspect ratio changes, always auto-center
3061
3533
  sessionConfig = new CameraSessionConfiguration(
3062
- sessionConfig.getDeviceId(),
3063
- sessionConfig.getPosition(),
3534
+ previousConfig.getDeviceId(),
3535
+ previousConfig.getPosition(),
3064
3536
  -1, // Force auto-center X (iOS: self.posX = -1)
3065
3537
  -1, // Force auto-center Y (iOS: self.posY = -1)
3066
- sessionConfig.getWidth(),
3067
- sessionConfig.getHeight(),
3068
- sessionConfig.getPaddingBottom(),
3069
- sessionConfig.getToBack(),
3070
- sessionConfig.getStoreToFile(),
3071
- sessionConfig.getEnableOpacity(),
3072
- sessionConfig.getDisableExifHeaderStripping(),
3073
- sessionConfig.getDisableAudio(),
3074
- sessionConfig.getZoomFactor(),
3538
+ previousConfig.getWidth(),
3539
+ previousConfig.getHeight(),
3540
+ previousConfig.getPaddingBottom(),
3541
+ previousConfig.getToBack(),
3542
+ previousConfig.getStoreToFile(),
3543
+ previousConfig.getEnableOpacity(),
3544
+ previousConfig.getDisableExifHeaderStripping(),
3545
+ previousConfig.getDisableAudio(),
3546
+ previousConfig.getZoomFactor(),
3075
3547
  aspectRatio,
3076
- sessionConfig.getAspectMode(),
3548
+ previousConfig.getAspectMode(),
3077
3549
  currentGridMode,
3078
- sessionConfig.getDisableFocusIndicator(),
3079
- sessionConfig.isVideoModeEnabled(),
3080
- sessionConfig.getVideoQuality()
3550
+ previousConfig.getDisableFocusIndicator(),
3551
+ previousConfig.isVideoModeEnabled(),
3552
+ previousConfig.getVideoQuality()
3081
3553
  );
3554
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3082
3555
  sessionConfig.setCentered(true);
3083
3556
 
3084
3557
  // Update layout and rebind camera with new aspect ratio
@@ -3121,27 +3594,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3121
3594
  public void setGridMode(String gridMode) {
3122
3595
  if (sessionConfig != null) {
3123
3596
  Log.d(TAG, "setGridMode: Changing grid mode to: " + gridMode);
3597
+ CameraSessionConfiguration previousConfig = sessionConfig;
3124
3598
  sessionConfig = new CameraSessionConfiguration(
3125
- sessionConfig.getDeviceId(),
3126
- sessionConfig.getPosition(),
3127
- sessionConfig.getX(),
3128
- sessionConfig.getY(),
3129
- sessionConfig.getWidth(),
3130
- sessionConfig.getHeight(),
3131
- sessionConfig.getPaddingBottom(),
3132
- sessionConfig.getToBack(),
3133
- sessionConfig.getStoreToFile(),
3134
- sessionConfig.getEnableOpacity(),
3135
- sessionConfig.getDisableExifHeaderStripping(),
3136
- sessionConfig.getDisableAudio(),
3137
- sessionConfig.getZoomFactor(),
3138
- sessionConfig.getAspectRatio(),
3139
- sessionConfig.getAspectMode(),
3599
+ previousConfig.getDeviceId(),
3600
+ previousConfig.getPosition(),
3601
+ previousConfig.getX(),
3602
+ previousConfig.getY(),
3603
+ previousConfig.getWidth(),
3604
+ previousConfig.getHeight(),
3605
+ previousConfig.getPaddingBottom(),
3606
+ previousConfig.getToBack(),
3607
+ previousConfig.getStoreToFile(),
3608
+ previousConfig.getEnableOpacity(),
3609
+ previousConfig.getDisableExifHeaderStripping(),
3610
+ previousConfig.getDisableAudio(),
3611
+ previousConfig.getZoomFactor(),
3612
+ previousConfig.getAspectRatio(),
3613
+ previousConfig.getAspectMode(),
3140
3614
  gridMode,
3141
- sessionConfig.getDisableFocusIndicator(),
3142
- sessionConfig.isVideoModeEnabled(),
3143
- sessionConfig.getVideoQuality()
3615
+ previousConfig.getDisableFocusIndicator(),
3616
+ previousConfig.isVideoModeEnabled(),
3617
+ previousConfig.getVideoQuality()
3144
3618
  );
3619
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3145
3620
 
3146
3621
  // Update the grid overlay immediately
3147
3622
  if (gridOverlayView != null) {
@@ -3461,27 +3936,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3461
3936
  );
3462
3937
  }
3463
3938
 
3939
+ CameraSessionConfiguration previousConfig = sessionConfig;
3464
3940
  sessionConfig = new CameraSessionConfiguration(
3465
- sessionConfig.getDeviceId(),
3466
- sessionConfig.getPosition(),
3941
+ previousConfig.getDeviceId(),
3942
+ previousConfig.getPosition(),
3467
3943
  params.leftMargin,
3468
3944
  params.topMargin,
3469
3945
  params.width,
3470
3946
  params.height,
3471
- sessionConfig.getPaddingBottom(),
3472
- sessionConfig.getToBack(),
3473
- sessionConfig.getStoreToFile(),
3474
- sessionConfig.getEnableOpacity(),
3475
- sessionConfig.getDisableExifHeaderStripping(),
3476
- sessionConfig.getDisableAudio(),
3477
- sessionConfig.getZoomFactor(),
3947
+ previousConfig.getPaddingBottom(),
3948
+ previousConfig.getToBack(),
3949
+ previousConfig.getStoreToFile(),
3950
+ previousConfig.getEnableOpacity(),
3951
+ previousConfig.getDisableExifHeaderStripping(),
3952
+ previousConfig.getDisableAudio(),
3953
+ previousConfig.getZoomFactor(),
3478
3954
  calculatedAspectRatio,
3479
- sessionConfig.getAspectMode(),
3480
- sessionConfig.getGridMode(),
3481
- sessionConfig.getDisableFocusIndicator(),
3482
- sessionConfig.isVideoModeEnabled(),
3483
- sessionConfig.getVideoQuality()
3955
+ previousConfig.getAspectMode(),
3956
+ previousConfig.getGridMode(),
3957
+ previousConfig.getDisableFocusIndicator(),
3958
+ previousConfig.isVideoModeEnabled(),
3959
+ previousConfig.getVideoQuality()
3484
3960
  );
3961
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3485
3962
 
3486
3963
  // If aspect ratio changed due to size update, rebind camera
3487
3964
  if (isRunning && !Objects.equals(currentAspectRatio, calculatedAspectRatio)) {