@capgo/camera-preview 7.4.0-alpha.3 → 7.4.0-alpha.32

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.
@@ -5,13 +5,19 @@ import static android.Manifest.permission.RECORD_AUDIO;
5
5
 
6
6
  import android.Manifest;
7
7
  import android.content.pm.ActivityInfo;
8
+ import android.content.res.Configuration;
9
+ import android.graphics.Color;
8
10
  import android.location.Location;
9
11
  import android.util.DisplayMetrics;
10
12
  import android.util.Log;
11
13
  import android.util.Size;
14
+ import android.view.OrientationEventListener;
12
15
  import android.view.View;
13
16
  import android.view.ViewGroup;
14
17
  import android.webkit.WebView;
18
+ import androidx.core.graphics.Insets;
19
+ import androidx.core.view.ViewCompat;
20
+ import androidx.core.view.WindowInsetsCompat;
15
21
  import com.ahm.capacitor.camera.preview.model.CameraDevice;
16
22
  import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
17
23
  import com.ahm.capacitor.camera.preview.model.LensInfo;
@@ -56,6 +62,8 @@ public class CameraPreview
56
62
  extends Plugin
57
63
  implements CameraXView.CameraXViewListener {
58
64
 
65
+ private static final String TAG = "CameraPreview CameraXView";
66
+
59
67
  static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
60
68
  static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
61
69
  static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS =
@@ -67,8 +75,31 @@ public class CameraPreview
67
75
  private int previousOrientationRequest =
68
76
  ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
69
77
  private CameraXView cameraXView;
78
+ private View rotationOverlay;
70
79
  private FusedLocationProviderClient fusedLocationClient;
71
80
  private Location lastLocation;
81
+ private OrientationEventListener orientationListener;
82
+ private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
83
+
84
+ @PluginMethod
85
+ public void getOrientation(PluginCall call) {
86
+ int orientation = getContext()
87
+ .getResources()
88
+ .getConfiguration()
89
+ .orientation;
90
+ String o;
91
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
92
+ // We don't distinguish upside-down reliably on Android, report generic portrait
93
+ o = "portrait";
94
+ } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
95
+ o = "landscape";
96
+ } else {
97
+ o = "unknown";
98
+ }
99
+ JSObject ret = new JSObject();
100
+ ret.put("orientation", o);
101
+ call.resolve(ret);
102
+ }
72
103
 
73
104
  @PluginMethod
74
105
  public void start(PluginCall call) {
@@ -205,6 +236,19 @@ public class CameraPreview
205
236
  .getActivity()
206
237
  .setRequestedOrientation(previousOrientationRequest);
207
238
 
239
+ // Disable and clear orientation listener
240
+ if (orientationListener != null) {
241
+ orientationListener.disable();
242
+ orientationListener = null;
243
+ lastOrientation = Configuration.ORIENTATION_UNDEFINED;
244
+ }
245
+
246
+ // Remove any rotation overlay if present
247
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
248
+ ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
249
+ rotationOverlay = null;
250
+ }
251
+
208
252
  if (cameraXView != null && cameraXView.isRunning()) {
209
253
  cameraXView.stopSession();
210
254
  cameraXView = null;
@@ -215,6 +259,10 @@ public class CameraPreview
215
259
 
216
260
  @PluginMethod
217
261
  public void getSupportedFlashModes(PluginCall call) {
262
+ if (cameraXView == null || !cameraXView.isRunning()) {
263
+ call.reject("Camera is not running");
264
+ return;
265
+ }
218
266
  List<String> supportedFlashModes = cameraXView.getSupportedFlashModes();
219
267
  JSArray jsonFlashModes = new JSArray();
220
268
  for (String mode : supportedFlashModes) {
@@ -268,6 +316,10 @@ public class CameraPreview
268
316
 
269
317
  @PluginMethod
270
318
  public void getZoom(PluginCall call) {
319
+ if (cameraXView == null || !cameraXView.isRunning()) {
320
+ call.reject("Camera is not running");
321
+ return;
322
+ }
271
323
  ZoomFactors zoomFactors = cameraXView.getZoomFactors();
272
324
  JSObject result = new JSObject();
273
325
  result.put("min", zoomFactors.getMin());
@@ -276,6 +328,57 @@ public class CameraPreview
276
328
  call.resolve(result);
277
329
  }
278
330
 
331
+ @PluginMethod
332
+ public void getZoomButtonValues(PluginCall call) {
333
+ if (cameraXView == null || !cameraXView.isRunning()) {
334
+ call.reject("Camera is not running");
335
+ return;
336
+ }
337
+ // Build a sorted set to dedupe and order ascending
338
+ java.util.Set<Double> sorted = new java.util.TreeSet<>();
339
+ sorted.add(1.0);
340
+ sorted.add(2.0);
341
+
342
+ // Try to detect ultra-wide to include its min zoom (often 0.5)
343
+ try {
344
+ List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
345
+ getContext()
346
+ );
347
+ ZoomFactors zoomFactors = cameraXView.getZoomFactors();
348
+ boolean hasUltraWide = false;
349
+ boolean hasTelephoto = false;
350
+ float minUltra = 0.5f;
351
+
352
+ for (CameraDevice device : devices) {
353
+ for (com.ahm.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
354
+ if ("ultraWide".equals(lens.getDeviceType())) {
355
+ hasUltraWide = true;
356
+ // Use overall minZoom for that device as the button value to represent UW
357
+ minUltra = Math.max(minUltra, zoomFactors.getMin());
358
+ } else if ("telephoto".equals(lens.getDeviceType())) {
359
+ hasTelephoto = true;
360
+ }
361
+ }
362
+ }
363
+ if (hasUltraWide) {
364
+ sorted.add((double) minUltra);
365
+ }
366
+ if (hasTelephoto) {
367
+ sorted.add(3.0);
368
+ }
369
+ } catch (Exception ignored) {
370
+ // Ignore and keep defaults
371
+ }
372
+
373
+ JSObject result = new JSObject();
374
+ JSArray values = new JSArray();
375
+ for (Double v : sorted) {
376
+ values.put(v);
377
+ }
378
+ result.put("values", values);
379
+ call.resolve(result);
380
+ }
381
+
279
382
  @PluginMethod
280
383
  public void setZoom(PluginCall call) {
281
384
  if (cameraXView == null || !cameraXView.isRunning()) {
@@ -287,9 +390,8 @@ public class CameraPreview
287
390
  call.reject("level parameter is required");
288
391
  return;
289
392
  }
290
- Boolean autoFocus = call.getBoolean("autoFocus", true);
291
393
  try {
292
- cameraXView.setZoom(level, autoFocus);
394
+ cameraXView.setZoom(level);
293
395
  call.resolve();
294
396
  } catch (Exception e) {
295
397
  call.reject("Failed to set zoom: " + e.getMessage());
@@ -922,6 +1024,229 @@ public class CameraPreview
922
1024
  bridge.saveCall(call);
923
1025
  cameraStartCallbackId = call.getCallbackId();
924
1026
  cameraXView.startSession(config);
1027
+
1028
+ // Setup orientation listener to mirror iOS screenResize emission
1029
+ if (orientationListener == null) {
1030
+ lastOrientation = getContext()
1031
+ .getResources()
1032
+ .getConfiguration()
1033
+ .orientation;
1034
+ orientationListener = new OrientationEventListener(getContext()) {
1035
+ @Override
1036
+ public void onOrientationChanged(int orientation) {
1037
+ if (orientation == ORIENTATION_UNKNOWN) return;
1038
+ int current = getContext()
1039
+ .getResources()
1040
+ .getConfiguration()
1041
+ .orientation;
1042
+ if (current != lastOrientation) {
1043
+ lastOrientation = current;
1044
+ // Post to next frame so WebView has updated bounds before we recompute layout
1045
+ getBridge()
1046
+ .getActivity()
1047
+ .getWindow()
1048
+ .getDecorView()
1049
+ .post(() -> handleOrientationChange());
1050
+ }
1051
+ }
1052
+ };
1053
+ if (orientationListener.canDetectOrientation()) {
1054
+ orientationListener.enable();
1055
+ }
1056
+ }
1057
+ });
1058
+ }
1059
+
1060
+ private void handleOrientationChange() {
1061
+ if (cameraXView == null || !cameraXView.isRunning()) return;
1062
+
1063
+ Log.d(
1064
+ TAG,
1065
+ "======================== ORIENTATION CHANGE DETECTED ========================"
1066
+ );
1067
+
1068
+ // Get comprehensive display and orientation information
1069
+ android.util.DisplayMetrics metrics = getContext()
1070
+ .getResources()
1071
+ .getDisplayMetrics();
1072
+ int screenWidthPx = metrics.widthPixels;
1073
+ int screenHeightPx = metrics.heightPixels;
1074
+ float density = metrics.density;
1075
+ int screenWidthDp = (int) (screenWidthPx / density);
1076
+ int screenHeightDp = (int) (screenHeightPx / density);
1077
+
1078
+ int current = getContext().getResources().getConfiguration().orientation;
1079
+ Log.d(TAG, "New orientation: " + current + " (1=PORTRAIT, 2=LANDSCAPE)");
1080
+ Log.d(
1081
+ TAG,
1082
+ "Screen dimensions - Pixels: " +
1083
+ screenWidthPx +
1084
+ "x" +
1085
+ screenHeightPx +
1086
+ ", DP: " +
1087
+ screenWidthDp +
1088
+ "x" +
1089
+ screenHeightDp +
1090
+ ", Density: " +
1091
+ density
1092
+ );
1093
+
1094
+ // Get WebView dimensions before rotation
1095
+ WebView webView = getBridge().getWebView();
1096
+ int webViewWidth = webView.getWidth();
1097
+ int webViewHeight = webView.getHeight();
1098
+ Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
1099
+
1100
+ // Get current preview bounds before rotation
1101
+ int[] oldBounds = cameraXView.getCurrentPreviewBounds();
1102
+ Log.d(
1103
+ TAG,
1104
+ "Current preview bounds before rotation: x=" +
1105
+ oldBounds[0] +
1106
+ ", y=" +
1107
+ oldBounds[1] +
1108
+ ", width=" +
1109
+ oldBounds[2] +
1110
+ ", height=" +
1111
+ oldBounds[3]
1112
+ );
1113
+
1114
+ getBridge()
1115
+ .getActivity()
1116
+ .runOnUiThread(() -> {
1117
+ // Create and show a black full-screen overlay during rotation
1118
+ ViewGroup rootView = (ViewGroup) getBridge()
1119
+ .getActivity()
1120
+ .getWindow()
1121
+ .getDecorView()
1122
+ .getRootView();
1123
+
1124
+ // Remove any existing overlay
1125
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1126
+ ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
1127
+ }
1128
+
1129
+ // Create new black overlay
1130
+ rotationOverlay = new View(getContext());
1131
+ rotationOverlay.setBackgroundColor(Color.BLACK);
1132
+ ViewGroup.LayoutParams overlayParams = new ViewGroup.LayoutParams(
1133
+ ViewGroup.LayoutParams.MATCH_PARENT,
1134
+ ViewGroup.LayoutParams.MATCH_PARENT
1135
+ );
1136
+ rotationOverlay.setLayoutParams(overlayParams);
1137
+ rootView.addView(rotationOverlay);
1138
+
1139
+ // Reapply current aspect ratio to recompute layout, then emit screenResize
1140
+ String ar = cameraXView.getAspectRatio();
1141
+ Log.d(TAG, "Reapplying aspect ratio: " + ar);
1142
+
1143
+ // Re-get dimensions after potential layout pass
1144
+ android.util.DisplayMetrics newMetrics = getContext()
1145
+ .getResources()
1146
+ .getDisplayMetrics();
1147
+ int newScreenWidthPx = newMetrics.widthPixels;
1148
+ int newScreenHeightPx = newMetrics.heightPixels;
1149
+ int newWebViewWidth = webView.getWidth();
1150
+ int newWebViewHeight = webView.getHeight();
1151
+
1152
+ Log.d(
1153
+ TAG,
1154
+ "New screen dimensions after rotation: " +
1155
+ newScreenWidthPx +
1156
+ "x" +
1157
+ newScreenHeightPx
1158
+ );
1159
+ Log.d(
1160
+ TAG,
1161
+ "New WebView dimensions after rotation: " +
1162
+ newWebViewWidth +
1163
+ "x" +
1164
+ newWebViewHeight
1165
+ );
1166
+
1167
+ // Force aspect ratio recalculation on orientation change
1168
+ cameraXView.forceAspectRatioRecalculation(ar, null, null, () -> {
1169
+ int[] bounds = cameraXView.getCurrentPreviewBounds();
1170
+ Log.d(
1171
+ TAG,
1172
+ "New bounds after orientation change: x=" +
1173
+ bounds[0] +
1174
+ ", y=" +
1175
+ bounds[1] +
1176
+ ", width=" +
1177
+ bounds[2] +
1178
+ ", height=" +
1179
+ bounds[3]
1180
+ );
1181
+ Log.d(
1182
+ TAG,
1183
+ "Bounds change: deltaX=" +
1184
+ (bounds[0] - oldBounds[0]) +
1185
+ ", deltaY=" +
1186
+ (bounds[1] - oldBounds[1]) +
1187
+ ", deltaWidth=" +
1188
+ (bounds[2] - oldBounds[2]) +
1189
+ ", deltaHeight=" +
1190
+ (bounds[3] - oldBounds[3])
1191
+ );
1192
+
1193
+ JSObject data = new JSObject();
1194
+ data.put("x", bounds[0]);
1195
+ data.put("y", bounds[1]);
1196
+ data.put("width", bounds[2]);
1197
+ data.put("height", bounds[3]);
1198
+ notifyListeners("screenResize", data);
1199
+
1200
+ // Also emit orientationChange with a unified string value
1201
+ String o;
1202
+ if (current == Configuration.ORIENTATION_PORTRAIT) {
1203
+ o = "portrait";
1204
+ } else if (current == Configuration.ORIENTATION_LANDSCAPE) {
1205
+ o = "landscape";
1206
+ } else {
1207
+ o = "unknown";
1208
+ }
1209
+ JSObject oData = new JSObject();
1210
+ oData.put("orientation", o);
1211
+ notifyListeners("orientationChange", oData);
1212
+
1213
+ // Don't remove the overlay here - wait for camera to fully start
1214
+ // The overlay will be removed after a delay to ensure camera is stable
1215
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1216
+ // Shorter delay for faster transition
1217
+ int delay = "4:3".equals(ar) ? 200 : 150;
1218
+ rotationOverlay.postDelayed(
1219
+ () -> {
1220
+ if (
1221
+ rotationOverlay != null && rotationOverlay.getParent() != null
1222
+ ) {
1223
+ rotationOverlay
1224
+ .animate()
1225
+ .alpha(0f)
1226
+ .setDuration(100) // Faster fade out
1227
+ .withEndAction(() -> {
1228
+ if (
1229
+ rotationOverlay != null &&
1230
+ rotationOverlay.getParent() != null
1231
+ ) {
1232
+ ((ViewGroup) rotationOverlay.getParent()).removeView(
1233
+ rotationOverlay
1234
+ );
1235
+ rotationOverlay = null;
1236
+ }
1237
+ })
1238
+ .start();
1239
+ }
1240
+ },
1241
+ delay
1242
+ );
1243
+ }
1244
+
1245
+ Log.d(
1246
+ TAG,
1247
+ "================================================================================"
1248
+ );
1249
+ });
925
1250
  });
926
1251
  }
927
1252
 
@@ -950,6 +1275,23 @@ public class CameraPreview
950
1275
  bridge.releaseCall(pluginCall);
951
1276
  }
952
1277
 
1278
+ private JSObject getViewSize(
1279
+ double x,
1280
+ double y,
1281
+ double width,
1282
+ double height
1283
+ ) {
1284
+ JSObject ret = new JSObject();
1285
+ // Return values with proper rounding to avoid gaps
1286
+ // For positions (x, y): ceil to avoid gaps at top/left
1287
+ // For dimensions (width, height): floor to avoid gaps at bottom/right
1288
+ ret.put("x", Math.ceil(x));
1289
+ ret.put("y", Math.ceil(y));
1290
+ ret.put("width", Math.floor(width));
1291
+ ret.put("height", Math.floor(height));
1292
+ return ret;
1293
+ }
1294
+
953
1295
  @Override
954
1296
  public void onCameraStarted(int width, int height, int x, int y) {
955
1297
  PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
@@ -1018,11 +1360,114 @@ public class CameraPreview
1018
1360
  Log.d("CameraPreview", "12. PIXEL RATIO - " + pixelRatio);
1019
1361
  Log.d("CameraPreview", "========================");
1020
1362
 
1021
- JSObject result = new JSObject();
1022
- result.put("width", width / pixelRatio);
1023
- result.put("height", height / pixelRatio);
1024
- result.put("x", x / pixelRatio);
1025
- result.put("y", relativeY / pixelRatio);
1363
+ // Calculate logical values with proper rounding to avoid sub-pixel issues
1364
+ double logicalWidth = width / pixelRatio;
1365
+ double logicalHeight = height / pixelRatio;
1366
+ double logicalX = x / pixelRatio;
1367
+ double logicalY = relativeY / pixelRatio;
1368
+
1369
+ JSObject result = getViewSize(
1370
+ logicalX,
1371
+ logicalY,
1372
+ logicalWidth,
1373
+ logicalHeight
1374
+ );
1375
+
1376
+ // Log exact calculations to debug one-pixel difference
1377
+ Log.d("CameraPreview", "========================");
1378
+ Log.d("CameraPreview", "FINAL POSITION CALCULATIONS:");
1379
+ Log.d(
1380
+ "CameraPreview",
1381
+ "Pixel values: x=" +
1382
+ x +
1383
+ ", y=" +
1384
+ relativeY +
1385
+ ", width=" +
1386
+ width +
1387
+ ", height=" +
1388
+ height
1389
+ );
1390
+ Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
1391
+ Log.d(
1392
+ "CameraPreview",
1393
+ "Logical values (exact): x=" +
1394
+ logicalX +
1395
+ ", y=" +
1396
+ logicalY +
1397
+ ", width=" +
1398
+ logicalWidth +
1399
+ ", height=" +
1400
+ logicalHeight
1401
+ );
1402
+ Log.d(
1403
+ "CameraPreview",
1404
+ "Logical values (rounded): x=" +
1405
+ Math.round(logicalX) +
1406
+ ", y=" +
1407
+ Math.round(logicalY) +
1408
+ ", width=" +
1409
+ Math.round(logicalWidth) +
1410
+ ", height=" +
1411
+ Math.round(logicalHeight)
1412
+ );
1413
+
1414
+ // Check if previewContainer has any padding or margin that might cause offset
1415
+ if (cameraXView != null) {
1416
+ View previewContainer = cameraXView.getPreviewContainer();
1417
+ if (previewContainer != null) {
1418
+ Log.d(
1419
+ "CameraPreview",
1420
+ "PreviewContainer padding: left=" +
1421
+ previewContainer.getPaddingLeft() +
1422
+ ", top=" +
1423
+ previewContainer.getPaddingTop() +
1424
+ ", right=" +
1425
+ previewContainer.getPaddingRight() +
1426
+ ", bottom=" +
1427
+ previewContainer.getPaddingBottom()
1428
+ );
1429
+ ViewGroup.LayoutParams params = previewContainer.getLayoutParams();
1430
+ if (params instanceof ViewGroup.MarginLayoutParams) {
1431
+ ViewGroup.MarginLayoutParams marginParams =
1432
+ (ViewGroup.MarginLayoutParams) params;
1433
+ Log.d(
1434
+ "CameraPreview",
1435
+ "PreviewContainer margins: left=" +
1436
+ marginParams.leftMargin +
1437
+ ", top=" +
1438
+ marginParams.topMargin +
1439
+ ", right=" +
1440
+ marginParams.rightMargin +
1441
+ ", bottom=" +
1442
+ marginParams.bottomMargin
1443
+ );
1444
+ }
1445
+ }
1446
+ }
1447
+ Log.d("CameraPreview", "========================");
1448
+
1449
+ // Log what we're returning
1450
+ Log.d(
1451
+ "CameraPreview",
1452
+ "Returning to JS - x: " +
1453
+ logicalX +
1454
+ " (from " +
1455
+ logicalX +
1456
+ "), y: " +
1457
+ logicalY +
1458
+ " (from " +
1459
+ logicalY +
1460
+ "), width: " +
1461
+ logicalWidth +
1462
+ " (from " +
1463
+ logicalWidth +
1464
+ "), height: " +
1465
+ logicalHeight +
1466
+ " (from " +
1467
+ logicalHeight +
1468
+ ")"
1469
+ );
1470
+
1026
1471
  call.resolve(result);
1027
1472
  bridge.releaseCall(call);
1028
1473
  cameraStartCallbackId = null; // Prevent re-use
@@ -1128,10 +1573,27 @@ public class CameraPreview
1128
1573
  float pixelRatio = metrics.density;
1129
1574
 
1130
1575
  JSObject ret = new JSObject();
1131
- ret.put("x", cameraXView.getPreviewX() / pixelRatio);
1132
- ret.put("y", cameraXView.getPreviewY() / pixelRatio);
1133
- ret.put("width", cameraXView.getPreviewWidth() / pixelRatio);
1134
- ret.put("height", cameraXView.getPreviewHeight() / pixelRatio);
1576
+ // Use same rounding strategy as start method
1577
+ double x = Math.ceil(cameraXView.getPreviewX() / pixelRatio);
1578
+ double y = Math.ceil(cameraXView.getPreviewY() / pixelRatio);
1579
+ double width = Math.floor(cameraXView.getPreviewWidth() / pixelRatio);
1580
+ double height = Math.floor(cameraXView.getPreviewHeight() / pixelRatio);
1581
+
1582
+ Log.d(
1583
+ "CameraPreview",
1584
+ "getPreviewSize: x=" +
1585
+ x +
1586
+ ", y=" +
1587
+ y +
1588
+ ", width=" +
1589
+ width +
1590
+ ", height=" +
1591
+ height
1592
+ );
1593
+ ret.put("x", x);
1594
+ ret.put("y", y);
1595
+ ret.put("width", width);
1596
+ ret.put("height", height);
1135
1597
  call.resolve(ret);
1136
1598
  }
1137
1599
 
@@ -1191,4 +1653,116 @@ public class CameraPreview
1191
1653
  call.resolve(ret);
1192
1654
  });
1193
1655
  }
1656
+
1657
+ @PluginMethod
1658
+ public void deleteFile(PluginCall call) {
1659
+ String path = call.getString("path");
1660
+ if (path == null || path.isEmpty()) {
1661
+ call.reject("path parameter is required");
1662
+ return;
1663
+ }
1664
+ try {
1665
+ java.io.File f = new java.io.File(android.net.Uri.parse(path).getPath());
1666
+ boolean deleted = f.exists() && f.delete();
1667
+ JSObject ret = new JSObject();
1668
+ ret.put("success", deleted);
1669
+ call.resolve(ret);
1670
+ } catch (Exception e) {
1671
+ call.reject("Failed to delete file: " + e.getMessage());
1672
+ }
1673
+ }
1674
+
1675
+ @PluginMethod
1676
+ public void getSafeAreaInsets(PluginCall call) {
1677
+ JSObject ret = new JSObject();
1678
+ int orientation = getContext()
1679
+ .getResources()
1680
+ .getConfiguration()
1681
+ .orientation;
1682
+
1683
+ int notchInsetPx = 0;
1684
+
1685
+ try {
1686
+ View decorView = getBridge().getActivity().getWindow().getDecorView();
1687
+ WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
1688
+
1689
+ if (insets != null) {
1690
+ // Get display cutout insets (notch, punch hole, etc.)
1691
+ // this.Capacitor.Plugins.CameraPreview.getSafeAreaInsets()
1692
+ Insets cutout = insets.getInsets(
1693
+ WindowInsetsCompat.Type.displayCutout()
1694
+ );
1695
+
1696
+ // Get system bars insets (status bar, navigation bars)
1697
+ Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
1698
+
1699
+ // In portrait mode, notch is at the top
1700
+ // In landscape mode, notch is typically at the left side (or right, but left is more common)
1701
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
1702
+ // Portrait: return top inset (notch/status bar)
1703
+ notchInsetPx = Math.max(cutout.top, sysBars.top);
1704
+
1705
+ // If no cutout detected but we have system bars, use status bar height as fallback
1706
+ if (cutout.top == 0 && sysBars.top > 0) {
1707
+ notchInsetPx = sysBars.top;
1708
+ }
1709
+ } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
1710
+ // Landscape: return left inset (notch moved to side)
1711
+ notchInsetPx = Math.max(cutout.left, sysBars.left);
1712
+
1713
+ // If no cutout detected but we have system bars, use left system bar as fallback
1714
+ if (cutout.left == 0 && sysBars.left > 0) {
1715
+ notchInsetPx = sysBars.left;
1716
+ }
1717
+
1718
+ // Additional fallback: some devices might have the notch on the right in landscape
1719
+ // If left is 0, check right side as well
1720
+ if (notchInsetPx == 0) {
1721
+ notchInsetPx = Math.max(cutout.right, sysBars.right);
1722
+ }
1723
+ } else {
1724
+ // Unknown orientation, default to top
1725
+ notchInsetPx = Math.max(cutout.top, sysBars.top);
1726
+ }
1727
+ } else {
1728
+ // Fallback to status bar height if WindowInsets are not available
1729
+ notchInsetPx = getStatusBarHeightPx();
1730
+ }
1731
+ } catch (Exception e) {
1732
+ // Final fallback
1733
+ notchInsetPx = getStatusBarHeightPx();
1734
+ }
1735
+
1736
+ // Convert pixels to dp for consistency with JS layout units
1737
+ float density = getContext().getResources().getDisplayMetrics().density;
1738
+ ret.put("orientation", orientation);
1739
+ ret.put("top", notchInsetPx / density);
1740
+ call.resolve(ret);
1741
+ }
1742
+
1743
+ private boolean approxEqualPx(int a, int b) {
1744
+ return Math.abs(a - b) <= 2; // within 2px tolerance
1745
+ }
1746
+
1747
+ private int getStatusBarHeightPx() {
1748
+ int result = 0;
1749
+ int resourceId = getContext()
1750
+ .getResources()
1751
+ .getIdentifier("status_bar_height", "dimen", "android");
1752
+ if (resourceId > 0) {
1753
+ result = getContext().getResources().getDimensionPixelSize(resourceId);
1754
+ }
1755
+ return result;
1756
+ }
1757
+
1758
+ private int getNavigationBarHeightPx() {
1759
+ int result = 0;
1760
+ int resourceId = getContext()
1761
+ .getResources()
1762
+ .getIdentifier("navigation_bar_height", "dimen", "android");
1763
+ if (resourceId > 0) {
1764
+ result = getContext().getResources().getDimensionPixelSize(resourceId);
1765
+ }
1766
+ return result;
1767
+ }
1194
1768
  }