@capgo/camera-preview 8.3.8 → 8.4.1

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.
@@ -46,9 +46,11 @@ import com.getcapacitor.annotation.Permission;
46
46
  import com.getcapacitor.annotation.PermissionCallback;
47
47
  import com.google.android.gms.location.FusedLocationProviderClient;
48
48
  import com.google.android.gms.location.LocationServices;
49
+ import java.util.ArrayList;
49
50
  import java.util.List;
50
51
  import java.util.Locale;
51
52
  import java.util.Objects;
53
+ import org.json.JSONArray;
52
54
  import org.json.JSONObject;
53
55
 
54
56
  @CapacitorPlugin(
@@ -108,6 +110,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
108
110
  cameraXView = null;
109
111
  }
110
112
  lastSessionConfig = null;
113
+ restoreSystemUiForToBackMode(getBridge().getActivity());
111
114
  }
112
115
 
113
116
  private CameraSessionConfiguration lastSessionConfig;
@@ -133,10 +136,17 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
133
136
  private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
134
137
  private String lastOrientationStr = "unknown";
135
138
  private boolean lastDisableAudio = true;
139
+ private boolean lastIncludeSafeAreaInsets = false;
136
140
  private Drawable originalWindowBackground;
137
141
  private Float originalWebViewAlpha;
138
142
  private Drawable originalWebViewParentBackground;
143
+ private Integer originalStatusBarColor;
144
+ private Integer originalNavigationBarColor;
145
+ private Boolean originalNavigationBarContrastEnforced;
139
146
  private boolean isCameraPermissionDialogShowing = false;
147
+ private boolean pendingStartBarcodeScanner = false;
148
+ private List<String> pendingStartBarcodeFormats = new ArrayList<>();
149
+ private int pendingStartBarcodeDetectionInterval = 500;
140
150
 
141
151
  @PluginMethod
142
152
  public void getExposureModes(PluginCall call) {
@@ -393,6 +403,95 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
393
403
  cameraXView.captureSample(quality);
394
404
  }
395
405
 
406
+ @PluginMethod
407
+ public void startBarcodeScanner(PluginCall call) {
408
+ if (cameraXView == null || !cameraXView.isRunning()) {
409
+ call.reject("Camera is not running");
410
+ return;
411
+ }
412
+
413
+ List<String> formats = getStringArray(call, "formats");
414
+ Integer detectionInterval = call.getInt("detectionInterval", 500);
415
+ cameraXView.startBarcodeScanner(
416
+ formats,
417
+ detectionInterval != null ? detectionInterval : 500,
418
+ new CameraXView.BarcodeScannerStartCallback() {
419
+ @Override
420
+ public void onStarted() {
421
+ call.resolve();
422
+ }
423
+
424
+ @Override
425
+ public void onError(String message) {
426
+ call.reject(message);
427
+ }
428
+ }
429
+ );
430
+ }
431
+
432
+ @PluginMethod
433
+ public void stopBarcodeScanner(PluginCall call) {
434
+ if (cameraXView != null) {
435
+ cameraXView.stopBarcodeScanner();
436
+ }
437
+ call.resolve();
438
+ }
439
+
440
+ private List<String> getStringArray(PluginCall call, String key) {
441
+ List<String> result = new ArrayList<>();
442
+ JSArray array = call.getArray(key);
443
+ if (array == null) {
444
+ return result;
445
+ }
446
+
447
+ for (int i = 0; i < array.length(); i++) {
448
+ String value = array.optString(i, null);
449
+ if (value != null && !value.isEmpty()) {
450
+ result.add(value);
451
+ }
452
+ }
453
+ return result;
454
+ }
455
+
456
+ private List<String> getStringArray(JSONObject object, String key) {
457
+ List<String> result = new ArrayList<>();
458
+ JSONArray array = object.optJSONArray(key);
459
+ if (array == null) {
460
+ return result;
461
+ }
462
+
463
+ for (int i = 0; i < array.length(); i++) {
464
+ String value = array.optString(i, null);
465
+ if (value != null && !value.isEmpty()) {
466
+ result.add(value);
467
+ }
468
+ }
469
+ return result;
470
+ }
471
+
472
+ private JSONObject getStartBarcodeScannerOptions(PluginCall call) {
473
+ Object barcodeScanner = call.getData().opt("barcodeScanner");
474
+ if (Boolean.TRUE.equals(barcodeScanner)) {
475
+ return new JSONObject();
476
+ }
477
+ if (barcodeScanner instanceof JSONObject) {
478
+ return (JSONObject) barcodeScanner;
479
+ }
480
+ return null;
481
+ }
482
+
483
+ private void setPendingStartBarcodeScanner(JSONObject options) {
484
+ pendingStartBarcodeScanner = options != null;
485
+ pendingStartBarcodeFormats = options != null ? getStringArray(options, "formats") : new ArrayList<>();
486
+ pendingStartBarcodeDetectionInterval = options != null ? options.optInt("detectionInterval", 500) : 500;
487
+ }
488
+
489
+ private void resetPendingStartBarcodeScanner() {
490
+ pendingStartBarcodeScanner = false;
491
+ pendingStartBarcodeFormats = new ArrayList<>();
492
+ pendingStartBarcodeDetectionInterval = 500;
493
+ }
494
+
396
495
  @PluginMethod
397
496
  public void stop(final PluginCall call) {
398
497
  boolean force = Boolean.TRUE.equals(call.getBoolean("force", false));
@@ -433,6 +532,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
433
532
  originalWindowBackground = null;
434
533
  }
435
534
  restoreWebViewVisualState();
535
+ restoreSystemUiForToBackMode(getBridge().getActivity());
436
536
  call.resolve();
437
537
  });
438
538
  }
@@ -901,6 +1001,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
901
1001
  final boolean lockOrientation = Boolean.TRUE.equals(call.getBoolean("lockAndroidOrientation", false));
902
1002
  final boolean disableAudio = Boolean.TRUE.equals(call.getBoolean("disableAudio", true));
903
1003
  this.lastDisableAudio = disableAudio;
1004
+ final boolean includeSafeAreaInsets = Boolean.TRUE.equals(call.getBoolean("includeSafeAreaInsets", false));
1005
+ this.lastIncludeSafeAreaInsets = includeSafeAreaInsets;
904
1006
  final String aspectRatio = call.getString("aspectRatio", "4:3");
905
1007
  final String aspectMode = call.getString("aspectMode", "contain");
906
1008
  final String gridMode = call.getString("gridMode", "none");
@@ -912,6 +1014,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
912
1014
  final boolean enableVideoMode = Boolean.TRUE.equals(call.getBoolean("enableVideoMode", false));
913
1015
  final boolean enablePhysicalDeviceSelection = Boolean.TRUE.equals(call.getBoolean("enablePhysicalDeviceSelection", false));
914
1016
  final String videoQuality = call.getString("videoQuality", "high");
1017
+ final JSONObject barcodeScannerOptions = getStartBarcodeScannerOptions(call);
915
1018
 
916
1019
  // Check for conflict between aspectRatio and size
917
1020
  if (call.getData().has("aspectRatio") && (call.getData().has("width") || call.getData().has("height"))) {
@@ -947,6 +1050,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
947
1050
  getBridge()
948
1051
  .getActivity()
949
1052
  .runOnUiThread(() -> {
1053
+ lockSystemUiForToBackMode(getBridge().getActivity(), toBack);
950
1054
  // Ensure transparent background when preview is behind the WebView (Android 10 fix)
951
1055
  if (toBack) {
952
1056
  try {
@@ -989,6 +1093,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
989
1093
  // we need to add that offset when placing native views
990
1094
  int webViewTopInset = webViewLocationOnScreen[1];
991
1095
  boolean isEdgeToEdgeActive = webViewLocationOnScreen[1] > 0;
1096
+ int safeAreaTopInsetPx = includeSafeAreaInsets ? getSafeAreaTopInsetPx() : 0;
992
1097
 
993
1098
  // Log all the positioning information for debugging
994
1099
  Log.d("CameraPreview", "WebView Position Debug:");
@@ -1044,6 +1149,10 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1044
1149
 
1045
1150
  Log.d("CameraPreview", " - Using webViewTopInset: " + webViewTopInset);
1046
1151
  Log.d("CameraPreview", " - isEdgeToEdgeActive: " + isEdgeToEdgeActive);
1152
+ Log.d(
1153
+ "CameraPreview",
1154
+ " - includeSafeAreaInsets: " + includeSafeAreaInsets + " (safeAreaTopInsetPx=" + safeAreaTopInsetPx + ")"
1155
+ );
1047
1156
 
1048
1157
  // Calculate position - center if x or y is -1
1049
1158
  int computedX;
@@ -1165,6 +1274,17 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1165
1274
  );
1166
1275
  }
1167
1276
 
1277
+ // Capacitor 8 edge-to-edge: WebView can be at y=0 while JS layout is below system bars.
1278
+ // If requested, apply the top system inset only when the WebView itself isn't already offset.
1279
+ if (!isEdgeToEdgeActive && includeSafeAreaInsets && safeAreaTopInsetPx > 0) {
1280
+ int before = computedY;
1281
+ computedY += safeAreaTopInsetPx;
1282
+ Log.d(
1283
+ "CameraPreview",
1284
+ "Safe-area adjustment: computedY " + before + " + safeAreaTopInsetPx " + safeAreaTopInsetPx + " = " + computedY
1285
+ );
1286
+ }
1287
+
1168
1288
  Log.d(
1169
1289
  "CameraPreview",
1170
1290
  "2b. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE (inset=" + webViewTopInset + ")" : "INACTIVE")
@@ -1217,6 +1337,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1217
1337
  config.setTargetZoom(finalTargetZoom);
1218
1338
  config.setCentered(isCentered);
1219
1339
  config.setEnablePhysicalDeviceSelection(enablePhysicalDeviceSelection);
1340
+ setPendingStartBarcodeScanner(barcodeScannerOptions);
1220
1341
 
1221
1342
  bridge.saveCall(call);
1222
1343
  cameraStartCallbackId = call.getCallbackId();
@@ -1288,6 +1409,18 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1288
1409
 
1289
1410
  // Get current preview bounds before rotation
1290
1411
  int[] oldBounds = cameraXView.getCurrentPreviewBounds();
1412
+ if (lastIncludeSafeAreaInsets) {
1413
+ int[] location = new int[2];
1414
+ webView.getLocationOnScreen(location);
1415
+ boolean isWebViewOffset = location[1] > 0;
1416
+ if (!isWebViewOffset) {
1417
+ int safeAreaTopInsetPx = getSafeAreaTopInsetPx();
1418
+ if (safeAreaTopInsetPx > 0) {
1419
+ int safeAreaTopInsetLogical = (int) Math.ceil(safeAreaTopInsetPx / density);
1420
+ oldBounds[1] = Math.max(0, oldBounds[1] - safeAreaTopInsetLogical);
1421
+ }
1422
+ }
1423
+ }
1291
1424
  Log.d(
1292
1425
  TAG,
1293
1426
  "Current preview bounds before rotation: x=" +
@@ -1338,6 +1471,18 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1338
1471
  // Force aspect ratio recalculation on orientation change
1339
1472
  cameraXView.forceAspectRatioRecalculation(ar, null, null, () -> {
1340
1473
  int[] bounds = cameraXView.getCurrentPreviewBounds();
1474
+ if (lastIncludeSafeAreaInsets) {
1475
+ int[] location = new int[2];
1476
+ webView.getLocationOnScreen(location);
1477
+ boolean isWebViewOffset = location[1] > 0;
1478
+ if (!isWebViewOffset) {
1479
+ int safeAreaTopInsetPx = getSafeAreaTopInsetPx();
1480
+ if (safeAreaTopInsetPx > 0) {
1481
+ int safeAreaTopInsetLogical = (int) Math.ceil(safeAreaTopInsetPx / density);
1482
+ bounds[1] = Math.max(0, bounds[1] - safeAreaTopInsetLogical);
1483
+ }
1484
+ }
1485
+ }
1341
1486
  Log.d(
1342
1487
  TAG,
1343
1488
  "New bounds after orientation change: x=" +
@@ -1531,6 +1676,71 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1531
1676
  return manufacturer.contains("xiaomi") || brand.contains("xiaomi") || brand.contains("redmi") || brand.contains("poco");
1532
1677
  }
1533
1678
 
1679
+ private int toOpaqueColor(int color) {
1680
+ return Color.argb(255, Color.red(color), Color.green(color), Color.blue(color));
1681
+ }
1682
+
1683
+ private void lockSystemUiForToBackMode(Activity activity, boolean toBack) {
1684
+ if (activity == null) {
1685
+ return;
1686
+ }
1687
+ if (!toBack) {
1688
+ restoreSystemUiForToBackMode(activity);
1689
+ return;
1690
+ }
1691
+
1692
+ try {
1693
+ if (originalStatusBarColor == null) {
1694
+ originalStatusBarColor = activity.getWindow().getStatusBarColor();
1695
+ }
1696
+ if (originalNavigationBarColor == null) {
1697
+ originalNavigationBarColor = activity.getWindow().getNavigationBarColor();
1698
+ }
1699
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && originalNavigationBarContrastEnforced == null) {
1700
+ originalNavigationBarContrastEnforced = activity.getWindow().isNavigationBarContrastEnforced();
1701
+ }
1702
+
1703
+ int statusBarColor = toOpaqueColor(originalStatusBarColor != null ? originalStatusBarColor : Color.BLACK);
1704
+ int navigationBarColor = toOpaqueColor(originalNavigationBarColor != null ? originalNavigationBarColor : Color.BLACK);
1705
+
1706
+ activity.getWindow().setStatusBarColor(statusBarColor);
1707
+ activity.getWindow().setNavigationBarColor(navigationBarColor);
1708
+
1709
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
1710
+ activity.getWindow().setNavigationBarContrastEnforced(false);
1711
+ }
1712
+ } catch (Exception e) {
1713
+ Log.w(TAG, "Failed to lock system UI colors for toBack mode", e);
1714
+ }
1715
+ }
1716
+
1717
+ private void restoreSystemUiForToBackMode(Activity activity) {
1718
+ final Integer statusBarColor = originalStatusBarColor;
1719
+ final Integer navigationBarColor = originalNavigationBarColor;
1720
+ final Boolean navigationBarContrastEnforced = originalNavigationBarContrastEnforced;
1721
+ originalStatusBarColor = null;
1722
+ originalNavigationBarColor = null;
1723
+ originalNavigationBarContrastEnforced = null;
1724
+
1725
+ if (activity == null) {
1726
+ return;
1727
+ }
1728
+
1729
+ activity.runOnUiThread(() -> {
1730
+ try {
1731
+ if (statusBarColor != null) {
1732
+ activity.getWindow().setStatusBarColor(statusBarColor);
1733
+ }
1734
+ if (navigationBarColor != null) {
1735
+ activity.getWindow().setNavigationBarColor(navigationBarColor);
1736
+ }
1737
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && navigationBarContrastEnforced != null) {
1738
+ activity.getWindow().setNavigationBarContrastEnforced(navigationBarContrastEnforced);
1739
+ }
1740
+ } catch (Exception ignored) {}
1741
+ });
1742
+ }
1743
+
1534
1744
  private void applyTransparentBackgroundsForToBack() {
1535
1745
  if (!isToBackMode()) {
1536
1746
  return;
@@ -1630,14 +1840,25 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1630
1840
  isEdgeToEdgeActive = webViewTopInset > 0;
1631
1841
  }
1632
1842
 
1633
- // Only convert to relative position if edge-to-edge is active
1634
- int relativeY = isEdgeToEdgeActive ? (y - webViewTopInset) : y;
1843
+ int safeAreaTopInsetPx = lastIncludeSafeAreaInsets ? getSafeAreaTopInsetPx() : 0;
1844
+
1845
+ // Only convert to relative position if WebView is offset or safe-area insets were applied.
1846
+ int relativeY = y;
1847
+ if (isEdgeToEdgeActive) {
1848
+ relativeY = y - webViewTopInset;
1849
+ } else if (lastIncludeSafeAreaInsets && safeAreaTopInsetPx > 0) {
1850
+ relativeY = y - safeAreaTopInsetPx;
1851
+ }
1635
1852
 
1636
1853
  Log.d("CameraPreview", "========================");
1637
1854
  Log.d("CameraPreview", "CAMERA STARTED - POSITION RETURNED:");
1638
1855
  Log.d("CameraPreview", "7. RETURNED (pixels) - x=" + x + ", y=" + y + ", width=" + width + ", height=" + height);
1639
1856
  Log.d("CameraPreview", "8. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE" : "INACTIVE"));
1640
1857
  Log.d("CameraPreview", "9. WEBVIEW INSET - " + webViewTopInset);
1858
+ Log.d(
1859
+ "CameraPreview",
1860
+ "9b. SAFE AREA - " + (lastIncludeSafeAreaInsets ? ("ENABLED (inset=" + safeAreaTopInsetPx + ")") : "DISABLED")
1861
+ );
1641
1862
  Log.d(
1642
1863
  "CameraPreview",
1643
1864
  "10. RELATIVE Y - " + relativeY + " (y=" + y + (isEdgeToEdgeActive ? " - inset=" + webViewTopInset : " unchanged") + ")"
@@ -1741,12 +1962,45 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1741
1962
  ")"
1742
1963
  );
1743
1964
 
1744
- call.resolve(result);
1745
- bridge.releaseCall(call);
1746
- cameraStartCallbackId = null; // Prevent re-use
1965
+ if (pendingStartBarcodeScanner && cameraXView != null) {
1966
+ List<String> formats = new ArrayList<>(pendingStartBarcodeFormats);
1967
+ int detectionInterval = pendingStartBarcodeDetectionInterval;
1968
+ cameraXView.startBarcodeScanner(
1969
+ formats,
1970
+ detectionInterval,
1971
+ new CameraXView.BarcodeScannerStartCallback() {
1972
+ @Override
1973
+ public void onStarted() {
1974
+ resolveCameraStartCall(call, result);
1975
+ }
1976
+
1977
+ @Override
1978
+ public void onError(String message) {
1979
+ rejectCameraStartCall(call, message);
1980
+ }
1981
+ }
1982
+ );
1983
+ return;
1984
+ }
1985
+
1986
+ resolveCameraStartCall(call, result);
1747
1987
  }
1748
1988
  }
1749
1989
 
1990
+ private void resolveCameraStartCall(PluginCall call, JSObject result) {
1991
+ call.resolve(result);
1992
+ bridge.releaseCall(call);
1993
+ cameraStartCallbackId = null; // Prevent re-use
1994
+ resetPendingStartBarcodeScanner();
1995
+ }
1996
+
1997
+ private void rejectCameraStartCall(PluginCall call, String message) {
1998
+ call.reject(message);
1999
+ bridge.releaseCall(call);
2000
+ cameraStartCallbackId = null;
2001
+ resetPendingStartBarcodeScanner();
2002
+ }
2003
+
1750
2004
  @Override
1751
2005
  public void onSampleTaken(String result) {
1752
2006
  PluginCall call = bridge.getSavedCall(sampleCallbackId);
@@ -1773,6 +2027,20 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1773
2027
  }
1774
2028
  }
1775
2029
 
2030
+ @Override
2031
+ public void onBarcodesScanned(org.json.JSONArray barcodes) {
2032
+ JSObject data = new JSObject();
2033
+ data.put("barcodes", barcodes);
2034
+ notifyListeners("barcodeScanned", data);
2035
+ }
2036
+
2037
+ @Override
2038
+ public void onBarcodeScanError(String message) {
2039
+ JSObject data = new JSObject();
2040
+ data.put("message", message);
2041
+ notifyListeners("barcodeScanError", data);
2042
+ }
2043
+
1776
2044
  @Override
1777
2045
  public void onCameraStartError(String message) {
1778
2046
  PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
@@ -1780,6 +2048,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1780
2048
  call.reject(message);
1781
2049
  bridge.releaseCall(call);
1782
2050
  cameraStartCallbackId = null;
2051
+ resetPendingStartBarcodeScanner();
1783
2052
  }
1784
2053
 
1785
2054
  // Restore original window background on error to prevent black screen
@@ -1801,6 +2070,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1801
2070
  }
1802
2071
  }
1803
2072
  restoreWebViewVisualState();
2073
+ restoreSystemUiForToBackMode(getBridge().getActivity());
1804
2074
  }
1805
2075
 
1806
2076
  @PluginMethod
@@ -1817,6 +2087,26 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1817
2087
  cameraXView.setAspectRatio(aspectRatio, x, y, () -> {
1818
2088
  // Return the actual preview bounds after layout and camera operations are complete
1819
2089
  int[] bounds = cameraXView.getCurrentPreviewBounds();
2090
+ if (lastIncludeSafeAreaInsets) {
2091
+ DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
2092
+ float pixelRatio = metrics.density;
2093
+ WebView webView = getBridge().getWebView();
2094
+ int webViewTopInset = 0;
2095
+ boolean isWebViewOffset = false;
2096
+ if (webView != null) {
2097
+ int[] location = new int[2];
2098
+ webView.getLocationOnScreen(location);
2099
+ webViewTopInset = location[1];
2100
+ isWebViewOffset = webViewTopInset > 0;
2101
+ }
2102
+ if (!isWebViewOffset) {
2103
+ int safeAreaTopInsetPx = getSafeAreaTopInsetPx();
2104
+ if (safeAreaTopInsetPx > 0) {
2105
+ int safeAreaTopInsetLogical = (int) Math.ceil(safeAreaTopInsetPx / pixelRatio);
2106
+ bounds[1] = Math.max(0, bounds[1] - safeAreaTopInsetLogical);
2107
+ }
2108
+ }
2109
+ }
1820
2110
  JSObject ret = new JSObject();
1821
2111
  ret.put("x", bounds[0]);
1822
2112
  ret.put("y", bounds[1]);
@@ -1874,6 +2164,17 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1874
2164
  DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
1875
2165
  float pixelRatio = metrics.density;
1876
2166
 
2167
+ WebView webView = getBridge().getWebView();
2168
+ int webViewTopInset = 0;
2169
+ boolean isWebViewOffset = false;
2170
+ if (webView != null) {
2171
+ int[] location = new int[2];
2172
+ webView.getLocationOnScreen(location);
2173
+ webViewTopInset = location[1];
2174
+ isWebViewOffset = webViewTopInset > 0;
2175
+ }
2176
+ int safeAreaTopInsetPx = lastIncludeSafeAreaInsets ? getSafeAreaTopInsetPx() : 0;
2177
+
1877
2178
  JSObject ret = new JSObject();
1878
2179
  // Use same rounding strategy as start method
1879
2180
  double x = Math.ceil(cameraXView.getPreviewX() / pixelRatio);
@@ -1881,6 +2182,11 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1881
2182
  double width = Math.floor(cameraXView.getPreviewWidth() / pixelRatio);
1882
2183
  double height = Math.floor(cameraXView.getPreviewHeight() / pixelRatio);
1883
2184
 
2185
+ if (!isWebViewOffset && lastIncludeSafeAreaInsets && safeAreaTopInsetPx > 0) {
2186
+ int safeAreaTopInsetLogical = (int) Math.ceil(safeAreaTopInsetPx / pixelRatio);
2187
+ y = Math.max(0, y - safeAreaTopInsetLogical);
2188
+ }
2189
+
1884
2190
  Log.d("CameraPreview", "getPreviewSize: x=" + x + ", y=" + y + ", width=" + width + ", height=" + height);
1885
2191
  ret.put("x", x);
1886
2192
  ret.put("y", y);
@@ -1909,20 +2215,25 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1909
2215
  // Check if edge-to-edge mode is active
1910
2216
  WebView webView = getBridge().getWebView();
1911
2217
  int webViewTopInset = 0;
1912
- boolean isEdgeToEdgeActive = false;
1913
2218
  if (webView != null) {
1914
2219
  int[] location = new int[2];
1915
2220
  webView.getLocationOnScreen(location);
1916
2221
  webViewTopInset = location[1];
1917
- isEdgeToEdgeActive = webViewTopInset > 0;
1918
2222
  }
2223
+ final boolean isWebViewOffset = webViewTopInset > 0;
2224
+ final int safeAreaTopInsetPx = lastIncludeSafeAreaInsets ? getSafeAreaTopInsetPx() : 0;
2225
+ final float pixelRatioFinal = pixelRatio;
1919
2226
 
1920
2227
  int x = (xParam != null && xParam > 0) ? (int) (xParam * pixelRatio) : 0;
1921
2228
  int y = (yParam != null && yParam > 0) ? (int) (yParam * pixelRatio) : 0;
1922
2229
 
1923
- // Add edge-to-edge inset to Y if active
1924
- if (isEdgeToEdgeActive && y > 0) {
2230
+ // Add inset to Y for coordinate conversion if needed.
2231
+ // - If the WebView is already offset from the screen top, use that.
2232
+ // - Otherwise, if safe-area insets were requested (Capacitor 8 edge-to-edge), use system inset.
2233
+ if (isWebViewOffset && y > 0) {
1925
2234
  y += webViewTopInset;
2235
+ } else if (!isWebViewOffset && lastIncludeSafeAreaInsets && safeAreaTopInsetPx > 0 && y > 0) {
2236
+ y += safeAreaTopInsetPx;
1926
2237
  }
1927
2238
  int width = (widthParam != null && widthParam > 0) ? (int) (widthParam * pixelRatio) : 0;
1928
2239
  int height = (heightParam != null && heightParam > 0) ? (int) (heightParam * pixelRatio) : 0;
@@ -1930,6 +2241,10 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1930
2241
  cameraXView.setPreviewSize(x, y, width, height, () -> {
1931
2242
  // Return the actual preview bounds after layout operations are complete
1932
2243
  int[] bounds = cameraXView.getCurrentPreviewBounds();
2244
+ if (!isWebViewOffset && lastIncludeSafeAreaInsets && safeAreaTopInsetPx > 0) {
2245
+ int safeAreaTopInsetLogical = (int) Math.ceil(safeAreaTopInsetPx / pixelRatioFinal);
2246
+ bounds[1] = Math.max(0, bounds[1] - safeAreaTopInsetLogical);
2247
+ }
1933
2248
  JSObject ret = new JSObject();
1934
2249
  ret.put("x", bounds[0]);
1935
2250
  ret.put("y", bounds[1]);
@@ -2103,6 +2418,19 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2103
2418
  return result;
2104
2419
  }
2105
2420
 
2421
+ private int getSafeAreaTopInsetPx() {
2422
+ try {
2423
+ View decorView = getBridge().getActivity().getWindow().getDecorView();
2424
+ WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
2425
+ if (insets != null) {
2426
+ Insets cutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout());
2427
+ Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
2428
+ return Math.max(cutout.top, sysBars.top);
2429
+ }
2430
+ } catch (Exception ignored) {}
2431
+ return getStatusBarHeightPx();
2432
+ }
2433
+
2106
2434
  @PluginMethod
2107
2435
  public void getPluginVersion(final PluginCall call) {
2108
2436
  try {