@capgo/camera-preview 7.4.0-beta.9 → 7.4.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.
Files changed (43) hide show
  1. package/README.md +242 -50
  2. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  3. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +1249 -143
  4. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +3400 -1432
  5. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +95 -58
  6. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
  7. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
  8. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +160 -72
  9. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
  10. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
  11. package/dist/docs.json +441 -40
  12. package/dist/esm/definitions.d.ts +167 -25
  13. package/dist/esm/definitions.js.map +1 -1
  14. package/dist/esm/index.d.ts +2 -0
  15. package/dist/esm/index.js +24 -1
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/web.d.ts +23 -3
  18. package/dist/esm/web.js +463 -65
  19. package/dist/esm/web.js.map +1 -1
  20. package/dist/plugin.cjs.js +485 -64
  21. package/dist/plugin.cjs.js.map +1 -1
  22. package/dist/plugin.js +485 -64
  23. package/dist/plugin.js.map +1 -1
  24. package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/CameraController.swift +731 -315
  25. package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +1902 -0
  26. package/package.json +11 -3
  27. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  28. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
  29. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  30. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  31. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  32. package/android/.gradle/8.14.2/fileChanges/last-build.bin +0 -0
  33. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  34. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  35. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  36. package/android/.gradle/8.14.2/gc.properties +0 -0
  37. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  38. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  39. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  40. package/android/.gradle/file-system.probe +0 -0
  41. package/android/.gradle/vcs-1/gc.properties +0 -0
  42. package/ios/Sources/CapgoCameraPreview/Plugin.swift +0 -1369
  43. /package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/GridOverlayView.swift +0 -0
@@ -5,9 +5,26 @@ 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;
10
+ import android.location.Location;
8
11
  import android.util.DisplayMetrics;
12
+ import android.util.Log;
13
+ import android.util.Size;
14
+ import android.view.OrientationEventListener;
15
+ import android.view.View;
16
+ import android.view.ViewGroup;
17
+ import android.webkit.WebView;
18
+ import androidx.core.graphics.Insets;
19
+ import androidx.core.view.ViewCompat;
20
+ import androidx.core.view.WindowInsetsCompat;
21
+ import com.ahm.capacitor.camera.preview.model.CameraDevice;
22
+ import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
23
+ import com.ahm.capacitor.camera.preview.model.LensInfo;
24
+ import com.ahm.capacitor.camera.preview.model.ZoomFactors;
9
25
  import com.getcapacitor.JSArray;
10
26
  import com.getcapacitor.JSObject;
27
+ import com.getcapacitor.Logger;
11
28
  import com.getcapacitor.PermissionState;
12
29
  import com.getcapacitor.Plugin;
13
30
  import com.getcapacitor.PluginCall;
@@ -15,22 +32,11 @@ import com.getcapacitor.PluginMethod;
15
32
  import com.getcapacitor.annotation.CapacitorPlugin;
16
33
  import com.getcapacitor.annotation.Permission;
17
34
  import com.getcapacitor.annotation.PermissionCallback;
18
- import com.ahm.capacitor.camera.preview.model.CameraDevice;
19
- import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
20
- import com.ahm.capacitor.camera.preview.model.ZoomFactors;
21
- import java.util.List;
22
- import java.util.Objects;
23
- import android.util.Size;
24
- import android.util.Log;
25
- import com.ahm.capacitor.camera.preview.model.LensInfo;
26
35
  import com.google.android.gms.location.FusedLocationProviderClient;
27
36
  import com.google.android.gms.location.LocationServices;
37
+ import java.util.List;
38
+ import java.util.Objects;
28
39
  import org.json.JSONObject;
29
- import android.location.Location;
30
- import android.view.ViewGroup;
31
-
32
- import com.getcapacitor.Logger;
33
-
34
40
 
35
41
  @CapacitorPlugin(
36
42
  name = "CameraPreview",
@@ -44,30 +50,62 @@ import com.getcapacitor.Logger;
44
50
  alias = CameraPreview.CAMERA_ONLY_PERMISSION_ALIAS
45
51
  ),
46
52
  @Permission(
47
- strings = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION },
53
+ strings = {
54
+ Manifest.permission.ACCESS_COARSE_LOCATION,
55
+ Manifest.permission.ACCESS_FINE_LOCATION,
56
+ },
48
57
  alias = CameraPreview.CAMERA_WITH_LOCATION_PERMISSION_ALIAS
49
- )
58
+ ),
50
59
  }
51
60
  )
52
61
  public class CameraPreview
53
62
  extends Plugin
54
63
  implements CameraXView.CameraXViewListener {
55
64
 
65
+ private static final String TAG = "CameraPreview CameraXView";
66
+
56
67
  static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
57
68
  static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
58
- static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS = "cameraWithLocation";
69
+ static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS =
70
+ "cameraWithLocation";
59
71
 
60
72
  private String captureCallbackId = "";
61
73
  private String snapshotCallbackId = "";
62
74
  private String cameraStartCallbackId = "";
63
- private int previousOrientationRequest = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
75
+ private int previousOrientationRequest =
76
+ ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
64
77
  private CameraXView cameraXView;
78
+ private View rotationOverlay;
65
79
  private FusedLocationProviderClient fusedLocationClient;
66
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
+ }
67
103
 
68
104
  @PluginMethod
69
105
  public void start(PluginCall call) {
70
- boolean disableAudio = Boolean.TRUE.equals(call.getBoolean("disableAudio", true));
106
+ boolean disableAudio = Boolean.TRUE.equals(
107
+ call.getBoolean("disableAudio", true)
108
+ );
71
109
  String permissionAlias = disableAudio
72
110
  ? CAMERA_ONLY_PERMISSION_ALIAS
73
111
  : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
@@ -103,8 +141,15 @@ public class CameraPreview
103
141
  final boolean withExifLocation = call.getBoolean("withExifLocation", false);
104
142
 
105
143
  if (withExifLocation) {
106
- if (getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) != PermissionState.GRANTED) {
107
- requestPermissionForAlias(CAMERA_WITH_LOCATION_PERMISSION_ALIAS, call, "captureWithLocationPermission");
144
+ if (
145
+ getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) !=
146
+ PermissionState.GRANTED
147
+ ) {
148
+ requestPermissionForAlias(
149
+ CAMERA_WITH_LOCATION_PERMISSION_ALIAS,
150
+ call,
151
+ "captureWithLocationPermission"
152
+ );
108
153
  } else {
109
154
  getLocationAndCapture(call);
110
155
  }
@@ -115,22 +160,32 @@ public class CameraPreview
115
160
 
116
161
  @PermissionCallback
117
162
  private void captureWithLocationPermission(PluginCall call) {
118
- if (getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) == PermissionState.GRANTED) {
163
+ if (
164
+ getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) ==
165
+ PermissionState.GRANTED
166
+ ) {
119
167
  getLocationAndCapture(call);
120
168
  } else {
121
- Logger.warn("Location permission denied. Capturing photo without location data.");
169
+ Logger.warn(
170
+ "Location permission denied. Capturing photo without location data."
171
+ );
122
172
  captureWithoutLocation(call);
123
173
  }
124
174
  }
125
175
 
126
176
  private void getLocationAndCapture(PluginCall call) {
127
- if (fusedLocationClient == null) {
128
- fusedLocationClient = LocationServices.getFusedLocationProviderClient(getContext());
129
- }
130
- fusedLocationClient.getLastLocation().addOnSuccessListener(getActivity(), location -> {
177
+ if (fusedLocationClient == null) {
178
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(
179
+ getContext()
180
+ );
181
+ }
182
+ fusedLocationClient
183
+ .getLastLocation()
184
+ .addOnSuccessListener(getActivity(), location -> {
131
185
  lastLocation = location;
132
186
  proceedWithCapture(call, lastLocation);
133
- }).addOnFailureListener(e -> {
187
+ })
188
+ .addOnFailureListener(e -> {
134
189
  Logger.error("Failed to get location: " + e.getMessage());
135
190
  proceedWithCapture(call, null);
136
191
  });
@@ -148,8 +203,16 @@ public class CameraPreview
148
203
  final boolean saveToGallery = call.getBoolean("saveToGallery", false);
149
204
  Integer width = call.getInt("width");
150
205
  Integer height = call.getInt("height");
206
+ String aspectRatio = call.getString("aspectRatio");
151
207
 
152
- cameraXView.capturePhoto(quality, saveToGallery, width, height, location);
208
+ cameraXView.capturePhoto(
209
+ quality,
210
+ saveToGallery,
211
+ width,
212
+ height,
213
+ aspectRatio,
214
+ location
215
+ );
153
216
  }
154
217
 
155
218
  @PluginMethod
@@ -168,23 +231,38 @@ public class CameraPreview
168
231
  public void stop(final PluginCall call) {
169
232
  bridge
170
233
  .getActivity()
171
- .runOnUiThread(
172
- () -> {
173
- getBridge()
174
- .getActivity()
175
- .setRequestedOrientation(previousOrientationRequest);
234
+ .runOnUiThread(() -> {
235
+ getBridge()
236
+ .getActivity()
237
+ .setRequestedOrientation(previousOrientationRequest);
176
238
 
177
- if (cameraXView != null && cameraXView.isRunning()) {
178
- cameraXView.stopSession();
179
- cameraXView = null;
180
- }
181
- call.resolve();
239
+ // Disable and clear orientation listener
240
+ if (orientationListener != null) {
241
+ orientationListener.disable();
242
+ orientationListener = null;
243
+ lastOrientation = Configuration.ORIENTATION_UNDEFINED;
182
244
  }
183
- );
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
+
252
+ if (cameraXView != null && cameraXView.isRunning()) {
253
+ cameraXView.stopSession();
254
+ cameraXView = null;
255
+ }
256
+ call.resolve();
257
+ });
184
258
  }
185
259
 
186
260
  @PluginMethod
187
261
  public void getSupportedFlashModes(PluginCall call) {
262
+ if (cameraXView == null || !cameraXView.isRunning()) {
263
+ call.reject("Camera is not running");
264
+ return;
265
+ }
188
266
  List<String> supportedFlashModes = cameraXView.getSupportedFlashModes();
189
267
  JSArray jsonFlashModes = new JSArray();
190
268
  for (String mode : supportedFlashModes) {
@@ -208,7 +286,9 @@ public class CameraPreview
208
286
 
209
287
  @PluginMethod
210
288
  public void getAvailableDevices(PluginCall call) {
211
- List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(getContext());
289
+ List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
290
+ getContext()
291
+ );
212
292
  JSArray devicesArray = new JSArray();
213
293
  for (CameraDevice device : devices) {
214
294
  JSObject deviceJson = new JSObject();
@@ -236,6 +316,10 @@ public class CameraPreview
236
316
 
237
317
  @PluginMethod
238
318
  public void getZoom(PluginCall call) {
319
+ if (cameraXView == null || !cameraXView.isRunning()) {
320
+ call.reject("Camera is not running");
321
+ return;
322
+ }
239
323
  ZoomFactors zoomFactors = cameraXView.getZoomFactors();
240
324
  JSObject result = new JSObject();
241
325
  result.put("min", zoomFactors.getMin());
@@ -244,6 +328,57 @@ public class CameraPreview
244
328
  call.resolve(result);
245
329
  }
246
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
+
247
382
  @PluginMethod
248
383
  public void setZoom(PluginCall call) {
249
384
  if (cameraXView == null || !cameraXView.isRunning()) {
@@ -263,6 +398,35 @@ public class CameraPreview
263
398
  }
264
399
  }
265
400
 
401
+ @PluginMethod
402
+ public void setFocus(PluginCall call) {
403
+ if (cameraXView == null || !cameraXView.isRunning()) {
404
+ call.reject("Camera is not running");
405
+ return;
406
+ }
407
+ Float x = call.getFloat("x");
408
+ Float y = call.getFloat("y");
409
+ if (x == null || y == null) {
410
+ call.reject("x and y parameters are required");
411
+ return;
412
+ }
413
+ // Reject if values are outside 0-1 range
414
+ if (x < 0f || x > 1f || y < 0f || y > 1f) {
415
+ call.reject("Focus coordinates must be between 0 and 1");
416
+ return;
417
+ }
418
+
419
+ getActivity()
420
+ .runOnUiThread(() -> {
421
+ try {
422
+ cameraXView.setFocus(x, y);
423
+ call.resolve();
424
+ } catch (Exception e) {
425
+ call.reject("Failed to set focus: " + e.getMessage());
426
+ }
427
+ });
428
+ }
429
+
266
430
  @PluginMethod
267
431
  public void setDeviceId(PluginCall call) {
268
432
  String deviceId = call.getString("deviceId");
@@ -285,7 +449,7 @@ public class CameraPreview
285
449
  JSObject rear = new JSObject();
286
450
  rear.put("facing", "rear");
287
451
  JSArray rearSizesJs = new JSArray();
288
- for(Size size : rearSizes) {
452
+ for (Size size : rearSizes) {
289
453
  JSObject sizeJs = new JSObject();
290
454
  sizeJs.put("width", size.getWidth());
291
455
  sizeJs.put("height", size.getHeight());
@@ -298,7 +462,7 @@ public class CameraPreview
298
462
  JSObject front = new JSObject();
299
463
  front.put("facing", "front");
300
464
  JSArray frontSizesJs = new JSArray();
301
- for(Size size : frontSizes) {
465
+ for (Size size : frontSizes) {
302
466
  JSObject sizeJs = new JSObject();
303
467
  sizeJs.put("width", size.getWidth());
304
468
  sizeJs.put("height", size.getHeight());
@@ -365,8 +529,14 @@ public class CameraPreview
365
529
 
366
530
  @PermissionCallback
367
531
  private void handleCameraPermissionResult(PluginCall call) {
368
- if (PermissionState.GRANTED.equals(getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)) ||
369
- PermissionState.GRANTED.equals(getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS))) {
532
+ if (
533
+ PermissionState.GRANTED.equals(
534
+ getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
535
+ ) ||
536
+ PermissionState.GRANTED.equals(
537
+ getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS)
538
+ )
539
+ ) {
370
540
  startCamera(call);
371
541
  } else {
372
542
  call.reject("Permission failed");
@@ -378,108 +548,706 @@ public class CameraPreview
378
548
  String originalDeviceId = call.getString("deviceId");
379
549
  String deviceId = originalDeviceId; // Use a mutable variable
380
550
 
381
- final String position = (positionParam == null || positionParam.isEmpty() || "rear".equals(positionParam) || "back".equals(positionParam)) ? "back" : "front";
382
- final int x = call.getInt("x", 0);
383
- final int y = call.getInt("y", 0);
551
+ final String position = (positionParam == null ||
552
+ positionParam.isEmpty() ||
553
+ "rear".equals(positionParam) ||
554
+ "back".equals(positionParam))
555
+ ? "back"
556
+ : "front";
557
+ // Use -1 as default to indicate centering is needed when x/y not provided
558
+ final Integer xParam = call.getInt("x");
559
+ final Integer yParam = call.getInt("y");
560
+ final int x = xParam != null ? xParam : -1;
561
+ final int y = yParam != null ? yParam : -1;
562
+
563
+ Log.d("CameraPreview", "========================");
564
+ Log.d("CameraPreview", "CAMERA POSITION TRACKING START:");
565
+ Log.d(
566
+ "CameraPreview",
567
+ "1. RAW PARAMS - xParam: " + xParam + ", yParam: " + yParam
568
+ );
569
+ Log.d(
570
+ "CameraPreview",
571
+ "2. AFTER DEFAULT - x: " +
572
+ x +
573
+ " (center=" +
574
+ (x == -1) +
575
+ "), y: " +
576
+ y +
577
+ " (center=" +
578
+ (y == -1) +
579
+ ")"
580
+ );
384
581
  final int width = call.getInt("width", 0);
385
582
  final int height = call.getInt("height", 0);
386
583
  final int paddingBottom = call.getInt("paddingBottom", 0);
387
584
  final boolean toBack = Boolean.TRUE.equals(call.getBoolean("toBack", true));
388
- final boolean storeToFile = Boolean.TRUE.equals(call.getBoolean("storeToFile", false));
389
- final boolean enableOpacity = Boolean.TRUE.equals(call.getBoolean("enableOpacity", false));
390
- final boolean enableZoom = Boolean.TRUE.equals(call.getBoolean("enableZoom", false));
391
- final boolean disableExifHeaderStripping = Boolean.TRUE.equals(call.getBoolean("disableExifHeaderStripping", false));
392
- final boolean lockOrientation = Boolean.TRUE.equals(call.getBoolean("lockAndroidOrientation", false));
393
- final boolean disableAudio = Boolean.TRUE.equals(call.getBoolean("disableAudio", true));
585
+ final boolean storeToFile = Boolean.TRUE.equals(
586
+ call.getBoolean("storeToFile", false)
587
+ );
588
+ final boolean enableOpacity = Boolean.TRUE.equals(
589
+ call.getBoolean("enableOpacity", false)
590
+ );
591
+ final boolean enableZoom = Boolean.TRUE.equals(
592
+ call.getBoolean("enableZoom", false)
593
+ );
594
+ final boolean disableExifHeaderStripping = Boolean.TRUE.equals(
595
+ call.getBoolean("disableExifHeaderStripping", false)
596
+ );
597
+ final boolean lockOrientation = Boolean.TRUE.equals(
598
+ call.getBoolean("lockAndroidOrientation", false)
599
+ );
600
+ final boolean disableAudio = Boolean.TRUE.equals(
601
+ call.getBoolean("disableAudio", true)
602
+ );
394
603
  final String aspectRatio = call.getString("aspectRatio", "4:3");
395
604
  final String gridMode = call.getString("gridMode", "none");
396
-
605
+ final String positioning = call.getString("positioning", "top");
606
+ final float initialZoomLevel = call.getFloat("initialZoomLevel", 1.0f);
607
+
397
608
  // Check for conflict between aspectRatio and size
398
- if (call.getData().has("aspectRatio") && (call.getData().has("width") || call.getData().has("height"))) {
399
- call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.");
609
+ if (
610
+ call.getData().has("aspectRatio") &&
611
+ (call.getData().has("width") || call.getData().has("height"))
612
+ ) {
613
+ call.reject(
614
+ "Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."
615
+ );
400
616
  return;
401
617
  }
402
618
 
403
- float targetZoom = 1.0f;
619
+ float targetZoom = initialZoomLevel;
404
620
  // Check if the selected device is a physical ultra-wide
405
621
  if (originalDeviceId != null) {
406
- List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(getContext());
407
- for (CameraDevice device : devices) {
408
- if (originalDeviceId.equals(device.getDeviceId()) && !device.isLogical()) {
409
- for (LensInfo lens : device.getLenses()) {
410
- if ("ultraWide".equals(lens.getDeviceType())) {
411
- Log.d("CameraPreview", "Ultra-wide lens selected. Targeting 0.5x zoom on logical camera.");
412
- targetZoom = 0.5f;
413
- // Force the use of the logical camera by clearing the specific deviceId
414
- deviceId = null;
415
- break;
416
- }
417
- }
622
+ List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
623
+ getContext()
624
+ );
625
+ for (CameraDevice device : devices) {
626
+ if (
627
+ originalDeviceId.equals(device.getDeviceId()) && !device.isLogical()
628
+ ) {
629
+ for (LensInfo lens : device.getLenses()) {
630
+ if ("ultraWide".equals(lens.getDeviceType())) {
631
+ Log.d(
632
+ "CameraPreview",
633
+ "Ultra-wide lens selected. Targeting 0.5x zoom on logical camera."
634
+ );
635
+ targetZoom = 0.5f;
636
+ // Force the use of the logical camera by clearing the specific deviceId
637
+ deviceId = null;
638
+ break;
418
639
  }
419
- if (deviceId == null) break; // Exit outer loop once we've made our decision
640
+ }
420
641
  }
642
+ if (deviceId == null) break; // Exit outer loop once we've made our decision
643
+ }
421
644
  }
422
645
 
423
- previousOrientationRequest = getBridge().getActivity().getRequestedOrientation();
646
+ previousOrientationRequest = getBridge()
647
+ .getActivity()
648
+ .getRequestedOrientation();
424
649
  cameraXView = new CameraXView(getContext(), getBridge().getWebView());
425
650
  cameraXView.setListener(this);
426
651
 
427
652
  String finalDeviceId = deviceId;
428
653
  float finalTargetZoom = targetZoom;
429
- getBridge().getActivity().runOnUiThread(() -> {
430
- DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
654
+ getBridge()
655
+ .getActivity()
656
+ .runOnUiThread(() -> {
657
+ DisplayMetrics metrics = getBridge()
658
+ .getActivity()
659
+ .getResources()
660
+ .getDisplayMetrics();
431
661
  if (lockOrientation) {
432
- getBridge().getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
662
+ getBridge()
663
+ .getActivity()
664
+ .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
433
665
  }
434
-
666
+
435
667
  // Debug: Let's check all the positioning information
436
- ViewGroup webViewParent = (ViewGroup) getBridge().getWebView().getParent();
437
-
668
+ ViewGroup webViewParent = (ViewGroup) getBridge()
669
+ .getWebView()
670
+ .getParent();
671
+
438
672
  // Get webview position in different coordinate systems
439
673
  int[] webViewLocationInWindow = new int[2];
440
674
  int[] webViewLocationOnScreen = new int[2];
441
675
  getBridge().getWebView().getLocationInWindow(webViewLocationInWindow);
442
676
  getBridge().getWebView().getLocationOnScreen(webViewLocationOnScreen);
443
-
677
+
444
678
  int webViewLeft = getBridge().getWebView().getLeft();
445
679
  int webViewTop = getBridge().getWebView().getTop();
446
-
680
+
447
681
  // Check parent position too
448
682
  int[] parentLocationInWindow = new int[2];
449
683
  int[] parentLocationOnScreen = new int[2];
450
684
  webViewParent.getLocationInWindow(parentLocationInWindow);
451
685
  webViewParent.getLocationOnScreen(parentLocationOnScreen);
452
-
686
+
453
687
  // Calculate pixel ratio
454
688
  float pixelRatio = metrics.density;
455
-
456
- // Try using just the pixel ratio without any webview offset for now
457
- int computedX = (int) (x * pixelRatio);
458
- int computedY = (int) (y * pixelRatio);
459
-
689
+
690
+ // The key insight: JavaScript coordinates are relative to the WebView's viewport
691
+ // If the WebView is positioned below the status bar (webViewLocationOnScreen[1] > 0),
692
+ // we need to add that offset when placing native views
693
+ int webViewTopInset = webViewLocationOnScreen[1];
694
+ boolean isEdgeToEdgeActive = webViewLocationOnScreen[1] > 0;
695
+
696
+ // Log all the positioning information for debugging
697
+ Log.d("CameraPreview", "WebView Position Debug:");
698
+ Log.d("CameraPreview", " - webView.getTop(): " + webViewTop);
699
+ Log.d("CameraPreview", " - webView.getLeft(): " + webViewLeft);
700
+ Log.d(
701
+ "CameraPreview",
702
+ " - webView locationInWindow: (" +
703
+ webViewLocationInWindow[0] +
704
+ ", " +
705
+ webViewLocationInWindow[1] +
706
+ ")"
707
+ );
708
+ Log.d(
709
+ "CameraPreview",
710
+ " - webView locationOnScreen: (" +
711
+ webViewLocationOnScreen[0] +
712
+ ", " +
713
+ webViewLocationOnScreen[1] +
714
+ ")"
715
+ );
716
+ Log.d(
717
+ "CameraPreview",
718
+ " - parent locationInWindow: (" +
719
+ parentLocationInWindow[0] +
720
+ ", " +
721
+ parentLocationInWindow[1] +
722
+ ")"
723
+ );
724
+ Log.d(
725
+ "CameraPreview",
726
+ " - parent locationOnScreen: (" +
727
+ parentLocationOnScreen[0] +
728
+ ", " +
729
+ parentLocationOnScreen[1] +
730
+ ")"
731
+ );
732
+
733
+ // Check if WebView has margins
734
+ View webView = getBridge().getWebView();
735
+ ViewGroup.LayoutParams webViewLayoutParams = webView.getLayoutParams();
736
+ if (webViewLayoutParams instanceof ViewGroup.MarginLayoutParams) {
737
+ ViewGroup.MarginLayoutParams marginParams =
738
+ (ViewGroup.MarginLayoutParams) webViewLayoutParams;
739
+ Log.d(
740
+ "CameraPreview",
741
+ " - webView margins: left=" +
742
+ marginParams.leftMargin +
743
+ ", top=" +
744
+ marginParams.topMargin +
745
+ ", right=" +
746
+ marginParams.rightMargin +
747
+ ", bottom=" +
748
+ marginParams.bottomMargin
749
+ );
750
+ }
751
+
752
+ // Check WebView padding
753
+ Log.d(
754
+ "CameraPreview",
755
+ " - webView padding: left=" +
756
+ webView.getPaddingLeft() +
757
+ ", top=" +
758
+ webView.getPaddingTop() +
759
+ ", right=" +
760
+ webView.getPaddingRight() +
761
+ ", bottom=" +
762
+ webView.getPaddingBottom()
763
+ );
764
+
765
+ Log.d("CameraPreview", " - Using webViewTopInset: " + webViewTopInset);
766
+ Log.d("CameraPreview", " - isEdgeToEdgeActive: " + isEdgeToEdgeActive);
767
+
768
+ // Calculate position - center if x or y is -1
769
+ int computedX;
770
+ int computedY;
771
+
772
+ // Calculate dimensions first
773
+ int computedWidth = width != 0
774
+ ? (int) (width * pixelRatio)
775
+ : getBridge().getWebView().getWidth();
776
+ int computedHeight = height != 0
777
+ ? (int) (height * pixelRatio)
778
+ : getBridge().getWebView().getHeight();
779
+ computedHeight -= (int) (paddingBottom * pixelRatio);
780
+
781
+ Log.d("CameraPreview", "========================");
782
+ Log.d("CameraPreview", "POSITIONING CALCULATIONS:");
783
+ Log.d(
784
+ "CameraPreview",
785
+ "1. INPUT - x: " +
786
+ x +
787
+ ", y: " +
788
+ y +
789
+ ", width: " +
790
+ width +
791
+ ", height: " +
792
+ height
793
+ );
794
+ Log.d("CameraPreview", "2. PIXEL RATIO: " + pixelRatio);
795
+ Log.d(
796
+ "CameraPreview",
797
+ "3. SCREEN - width: " +
798
+ metrics.widthPixels +
799
+ ", height: " +
800
+ metrics.heightPixels
801
+ );
802
+ Log.d(
803
+ "CameraPreview",
804
+ "4. WEBVIEW - width: " +
805
+ getBridge().getWebView().getWidth() +
806
+ ", height: " +
807
+ getBridge().getWebView().getHeight()
808
+ );
809
+ Log.d(
810
+ "CameraPreview",
811
+ "5. COMPUTED DIMENSIONS - width: " +
812
+ computedWidth +
813
+ ", height: " +
814
+ computedHeight
815
+ );
816
+
817
+ if (x == -1) {
818
+ // Center horizontally
819
+ int screenWidth = metrics.widthPixels;
820
+ computedX = (screenWidth - computedWidth) / 2;
821
+ Log.d(
822
+ "CameraPreview",
823
+ "Centering horizontally: screenWidth=" +
824
+ screenWidth +
825
+ ", computedWidth=" +
826
+ computedWidth +
827
+ ", computedX=" +
828
+ computedX
829
+ );
830
+ } else {
831
+ computedX = (int) (x * pixelRatio);
832
+ Log.d(
833
+ "CameraPreview",
834
+ "Using provided X position: " +
835
+ x +
836
+ " * " +
837
+ pixelRatio +
838
+ " = " +
839
+ computedX
840
+ );
841
+ }
842
+
843
+ if (y == -1) {
844
+ // Position vertically based on positioning parameter
845
+ int screenHeight = metrics.heightPixels;
846
+
847
+ switch (positioning) {
848
+ case "top":
849
+ computedY = 0;
850
+ Log.d("CameraPreview", "Positioning at top: computedY=0");
851
+ break;
852
+ case "bottom":
853
+ computedY = screenHeight - computedHeight;
854
+ Log.d(
855
+ "CameraPreview",
856
+ "Positioning at bottom: screenHeight=" +
857
+ screenHeight +
858
+ ", computedHeight=" +
859
+ computedHeight +
860
+ ", computedY=" +
861
+ computedY
862
+ );
863
+ break;
864
+ case "center":
865
+ default:
866
+ // Center vertically
867
+ if (isEdgeToEdgeActive) {
868
+ // When WebView is offset from top, center within the available space
869
+ // The camera should be centered in the full screen, not just the WebView area
870
+ computedY = (screenHeight - computedHeight) / 2;
871
+ Log.d(
872
+ "CameraPreview",
873
+ "Centering vertically with WebView offset: screenHeight=" +
874
+ screenHeight +
875
+ ", webViewTop=" +
876
+ webViewTopInset +
877
+ ", computedHeight=" +
878
+ computedHeight +
879
+ ", computedY=" +
880
+ computedY
881
+ );
882
+ } else {
883
+ // Normal mode - use full screen height
884
+ computedY = (screenHeight - computedHeight) / 2;
885
+ Log.d(
886
+ "CameraPreview",
887
+ "Centering vertically (normal): screenHeight=" +
888
+ screenHeight +
889
+ ", computedHeight=" +
890
+ computedHeight +
891
+ ", computedY=" +
892
+ computedY
893
+ );
894
+ }
895
+ break;
896
+ }
897
+ } else {
898
+ computedY = (int) (y * pixelRatio);
899
+ // If edge-to-edge is active, JavaScript Y is relative to WebView content area
900
+ // We need to add the inset to get absolute screen position
901
+ if (isEdgeToEdgeActive) {
902
+ computedY += webViewTopInset;
903
+ Log.d(
904
+ "CameraPreview",
905
+ "Edge-to-edge adjustment: Y position " +
906
+ (int) (y * pixelRatio) +
907
+ " + inset " +
908
+ webViewTopInset +
909
+ " = " +
910
+ computedY
911
+ );
912
+ }
913
+ Log.d(
914
+ "CameraPreview",
915
+ "Using provided Y position: " +
916
+ y +
917
+ " * " +
918
+ pixelRatio +
919
+ " = " +
920
+ computedY +
921
+ (isEdgeToEdgeActive ? " (adjusted for edge-to-edge)" : "")
922
+ );
923
+ }
924
+
925
+ Log.d(
926
+ "CameraPreview",
927
+ "2b. EDGE-TO-EDGE - " +
928
+ (isEdgeToEdgeActive
929
+ ? "ACTIVE (inset=" + webViewTopInset + ")"
930
+ : "INACTIVE")
931
+ );
932
+ Log.d(
933
+ "CameraPreview",
934
+ "3. COMPUTED POSITION - x=" + computedX + ", y=" + computedY
935
+ );
936
+ Log.d(
937
+ "CameraPreview",
938
+ "4. COMPUTED SIZE - width=" +
939
+ computedWidth +
940
+ ", height=" +
941
+ computedHeight
942
+ );
460
943
  Log.d("CameraPreview", "=== COORDINATE DEBUG ===");
461
- Log.d("CameraPreview", "WebView getLeft/getTop: (" + webViewLeft + ", " + webViewTop + ")");
462
- Log.d("CameraPreview", "WebView locationInWindow: (" + webViewLocationInWindow[0] + ", " + webViewLocationInWindow[1] + ")");
463
- Log.d("CameraPreview", "WebView locationOnScreen: (" + webViewLocationOnScreen[0] + ", " + webViewLocationOnScreen[1] + ")");
464
- Log.d("CameraPreview", "Parent locationInWindow: (" + parentLocationInWindow[0] + ", " + parentLocationInWindow[1] + ")");
465
- Log.d("CameraPreview", "Parent locationOnScreen: (" + parentLocationOnScreen[0] + ", " + parentLocationOnScreen[1] + ")");
466
- Log.d("CameraPreview", "Parent class: " + webViewParent.getClass().getSimpleName());
467
- Log.d("CameraPreview", "Requested position (logical): (" + x + ", " + y + ")");
944
+ Log.d(
945
+ "CameraPreview",
946
+ "WebView getLeft/getTop: (" + webViewLeft + ", " + webViewTop + ")"
947
+ );
948
+ Log.d(
949
+ "CameraPreview",
950
+ "WebView locationInWindow: (" +
951
+ webViewLocationInWindow[0] +
952
+ ", " +
953
+ webViewLocationInWindow[1] +
954
+ ")"
955
+ );
956
+ Log.d(
957
+ "CameraPreview",
958
+ "WebView locationOnScreen: (" +
959
+ webViewLocationOnScreen[0] +
960
+ ", " +
961
+ webViewLocationOnScreen[1] +
962
+ ")"
963
+ );
964
+ Log.d(
965
+ "CameraPreview",
966
+ "Parent locationInWindow: (" +
967
+ parentLocationInWindow[0] +
968
+ ", " +
969
+ parentLocationInWindow[1] +
970
+ ")"
971
+ );
972
+ Log.d(
973
+ "CameraPreview",
974
+ "Parent locationOnScreen: (" +
975
+ parentLocationOnScreen[0] +
976
+ ", " +
977
+ parentLocationOnScreen[1] +
978
+ ")"
979
+ );
980
+ Log.d(
981
+ "CameraPreview",
982
+ "Parent class: " + webViewParent.getClass().getSimpleName()
983
+ );
984
+ Log.d(
985
+ "CameraPreview",
986
+ "Requested position (logical): (" + x + ", " + y + ")"
987
+ );
468
988
  Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
469
- Log.d("CameraPreview", "Final computed position (no offset): (" + computedX + ", " + computedY + ")");
989
+ Log.d(
990
+ "CameraPreview",
991
+ "Final computed position (no offset): (" +
992
+ computedX +
993
+ ", " +
994
+ computedY +
995
+ ")"
996
+ );
997
+ Log.d("CameraPreview", "5. IS_CENTERED - " + (x == -1 || y == -1));
470
998
  Log.d("CameraPreview", "========================");
471
- int computedWidth = width != 0 ? (int) (width * pixelRatio) : getBridge().getWebView().getWidth();
472
- int computedHeight = height != 0 ? (int) (height * pixelRatio) : getBridge().getWebView().getHeight();
473
- computedHeight -= (int) (paddingBottom * pixelRatio);
474
999
 
475
- CameraSessionConfiguration config = new CameraSessionConfiguration(finalDeviceId, position, computedX, computedY, computedWidth, computedHeight, paddingBottom, toBack, storeToFile, enableOpacity, enableZoom, disableExifHeaderStripping, disableAudio, 1.0f, aspectRatio, gridMode);
1000
+ // Pass along whether we're centering so CameraXView knows not to add insets
1001
+ boolean isCentered = (x == -1 || y == -1);
1002
+
1003
+ CameraSessionConfiguration config = new CameraSessionConfiguration(
1004
+ finalDeviceId,
1005
+ position,
1006
+ computedX,
1007
+ computedY,
1008
+ computedWidth,
1009
+ computedHeight,
1010
+ paddingBottom,
1011
+ toBack,
1012
+ storeToFile,
1013
+ enableOpacity,
1014
+ enableZoom,
1015
+ disableExifHeaderStripping,
1016
+ disableAudio,
1017
+ 1.0f,
1018
+ aspectRatio,
1019
+ gridMode
1020
+ );
476
1021
  config.setTargetZoom(finalTargetZoom);
1022
+ config.setCentered(isCentered);
477
1023
 
478
1024
  bridge.saveCall(call);
479
1025
  cameraStartCallbackId = call.getCallbackId();
480
1026
  cameraXView.startSession(config);
481
- }
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]
482
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
+ });
1250
+ });
483
1251
  }
484
1252
 
485
1253
  @Override
@@ -507,22 +1275,202 @@ public class CameraPreview
507
1275
  bridge.releaseCall(pluginCall);
508
1276
  }
509
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
+
510
1295
  @Override
511
1296
  public void onCameraStarted(int width, int height, int x, int y) {
512
1297
  PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
513
1298
  if (call != null) {
514
- // Convert pixel values back to logical units
515
- DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
516
- float pixelRatio = metrics.density;
517
-
518
- JSObject result = new JSObject();
519
- result.put("width", width / pixelRatio);
520
- result.put("height", height / pixelRatio);
521
- result.put("x", x / pixelRatio);
522
- result.put("y", y / pixelRatio);
523
- call.resolve(result);
524
- bridge.releaseCall(call);
525
- cameraStartCallbackId = null; // Prevent re-use
1299
+ // Convert pixel values back to logical units
1300
+ DisplayMetrics metrics = getBridge()
1301
+ .getActivity()
1302
+ .getResources()
1303
+ .getDisplayMetrics();
1304
+ float pixelRatio = metrics.density;
1305
+
1306
+ // When WebView is offset from the top (e.g., below status bar),
1307
+ // we need to convert between JavaScript coordinates (relative to WebView)
1308
+ // and native coordinates (relative to screen)
1309
+ WebView webView = getBridge().getWebView();
1310
+ int webViewTopInset = 0;
1311
+ boolean isEdgeToEdgeActive = false;
1312
+ if (webView != null) {
1313
+ int[] location = new int[2];
1314
+ webView.getLocationOnScreen(location);
1315
+ webViewTopInset = location[1];
1316
+ isEdgeToEdgeActive = webViewTopInset > 0;
1317
+ }
1318
+
1319
+ // Only convert to relative position if edge-to-edge is active
1320
+ int relativeY = isEdgeToEdgeActive ? (y - webViewTopInset) : y;
1321
+
1322
+ Log.d("CameraPreview", "========================");
1323
+ Log.d("CameraPreview", "CAMERA STARTED - POSITION RETURNED:");
1324
+ Log.d(
1325
+ "CameraPreview",
1326
+ "7. RETURNED (pixels) - x=" +
1327
+ x +
1328
+ ", y=" +
1329
+ y +
1330
+ ", width=" +
1331
+ width +
1332
+ ", height=" +
1333
+ height
1334
+ );
1335
+ Log.d(
1336
+ "CameraPreview",
1337
+ "8. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE" : "INACTIVE")
1338
+ );
1339
+ Log.d("CameraPreview", "9. WEBVIEW INSET - " + webViewTopInset);
1340
+ Log.d(
1341
+ "CameraPreview",
1342
+ "10. RELATIVE Y - " +
1343
+ relativeY +
1344
+ " (y=" +
1345
+ y +
1346
+ (isEdgeToEdgeActive ? " - inset=" + webViewTopInset : " unchanged") +
1347
+ ")"
1348
+ );
1349
+ Log.d(
1350
+ "CameraPreview",
1351
+ "11. RETURNED (logical) - x=" +
1352
+ (x / pixelRatio) +
1353
+ ", y=" +
1354
+ (relativeY / pixelRatio) +
1355
+ ", width=" +
1356
+ (width / pixelRatio) +
1357
+ ", height=" +
1358
+ (height / pixelRatio)
1359
+ );
1360
+ Log.d("CameraPreview", "12. PIXEL RATIO - " + pixelRatio);
1361
+ Log.d("CameraPreview", "========================");
1362
+
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
+
1471
+ call.resolve(result);
1472
+ bridge.releaseCall(call);
1473
+ cameraStartCallbackId = null; // Prevent re-use
526
1474
  }
527
1475
  }
528
1476
 
@@ -542,9 +1490,9 @@ public class CameraPreview
542
1490
  public void onCameraStartError(String message) {
543
1491
  PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
544
1492
  if (call != null) {
545
- call.reject(message);
546
- bridge.releaseCall(call);
547
- cameraStartCallbackId = null;
1493
+ call.reject(message);
1494
+ bridge.releaseCall(call);
1495
+ cameraStartCallbackId = null;
548
1496
  }
549
1497
  }
550
1498
 
@@ -557,19 +1505,20 @@ public class CameraPreview
557
1505
  String aspectRatio = call.getString("aspectRatio", "4:3");
558
1506
  Float x = call.getFloat("x");
559
1507
  Float y = call.getFloat("y");
560
-
561
- getActivity().runOnUiThread(() -> {
562
- cameraXView.setAspectRatio(aspectRatio, x, y, () -> {
563
- // Return the actual preview bounds after layout and camera operations are complete
564
- int[] bounds = cameraXView.getCurrentPreviewBounds();
565
- JSObject ret = new JSObject();
566
- ret.put("x", bounds[0]);
567
- ret.put("y", bounds[1]);
568
- ret.put("width", bounds[2]);
569
- ret.put("height", bounds[3]);
570
- call.resolve(ret);
1508
+
1509
+ getActivity()
1510
+ .runOnUiThread(() -> {
1511
+ cameraXView.setAspectRatio(aspectRatio, x, y, () -> {
1512
+ // Return the actual preview bounds after layout and camera operations are complete
1513
+ int[] bounds = cameraXView.getCurrentPreviewBounds();
1514
+ JSObject ret = new JSObject();
1515
+ ret.put("x", bounds[0]);
1516
+ ret.put("y", bounds[1]);
1517
+ ret.put("width", bounds[2]);
1518
+ ret.put("height", bounds[3]);
1519
+ call.resolve(ret);
1520
+ });
571
1521
  });
572
- });
573
1522
  }
574
1523
 
575
1524
  @PluginMethod
@@ -591,10 +1540,11 @@ public class CameraPreview
591
1540
  return;
592
1541
  }
593
1542
  String gridMode = call.getString("gridMode", "none");
594
- getActivity().runOnUiThread(() -> {
595
- cameraXView.setGridMode(gridMode);
596
- call.resolve();
597
- });
1543
+ getActivity()
1544
+ .runOnUiThread(() -> {
1545
+ cameraXView.setGridMode(gridMode);
1546
+ call.resolve();
1547
+ });
598
1548
  }
599
1549
 
600
1550
  @PluginMethod
@@ -614,40 +1564,84 @@ public class CameraPreview
614
1564
  call.reject("Camera is not running");
615
1565
  return;
616
1566
  }
617
-
1567
+
618
1568
  // Convert pixel values back to logical units
619
- DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
1569
+ DisplayMetrics metrics = getBridge()
1570
+ .getActivity()
1571
+ .getResources()
1572
+ .getDisplayMetrics();
620
1573
  float pixelRatio = metrics.density;
621
-
1574
+
622
1575
  JSObject ret = new JSObject();
623
- ret.put("x", cameraXView.getPreviewX() / pixelRatio);
624
- ret.put("y", cameraXView.getPreviewY() / pixelRatio);
625
- ret.put("width", cameraXView.getPreviewWidth() / pixelRatio);
626
- 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);
627
1597
  call.resolve(ret);
628
1598
  }
1599
+
629
1600
  @PluginMethod
630
1601
  public void setPreviewSize(PluginCall call) {
631
1602
  if (cameraXView == null || !cameraXView.isRunning()) {
632
1603
  call.reject("Camera is not running");
633
1604
  return;
634
1605
  }
635
-
1606
+
636
1607
  // Get values from call - null values will become 0
637
1608
  Integer xParam = call.getInt("x");
638
1609
  Integer yParam = call.getInt("y");
639
1610
  Integer widthParam = call.getInt("width");
640
1611
  Integer heightParam = call.getInt("height");
641
-
1612
+
642
1613
  // Apply pixel ratio conversion to non-null values
643
- DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
1614
+ DisplayMetrics metrics = getBridge()
1615
+ .getActivity()
1616
+ .getResources()
1617
+ .getDisplayMetrics();
644
1618
  float pixelRatio = metrics.density;
645
-
1619
+
1620
+ // Check if edge-to-edge mode is active
1621
+ WebView webView = getBridge().getWebView();
1622
+ int webViewTopInset = 0;
1623
+ boolean isEdgeToEdgeActive = false;
1624
+ if (webView != null) {
1625
+ int[] location = new int[2];
1626
+ webView.getLocationOnScreen(location);
1627
+ webViewTopInset = location[1];
1628
+ isEdgeToEdgeActive = webViewTopInset > 0;
1629
+ }
1630
+
646
1631
  int x = (xParam != null && xParam > 0) ? (int) (xParam * pixelRatio) : 0;
647
1632
  int y = (yParam != null && yParam > 0) ? (int) (yParam * pixelRatio) : 0;
648
- int width = (widthParam != null && widthParam > 0) ? (int) (widthParam * pixelRatio) : 0;
649
- int height = (heightParam != null && heightParam > 0) ? (int) (heightParam * pixelRatio) : 0;
650
-
1633
+
1634
+ // Add edge-to-edge inset to Y if active
1635
+ if (isEdgeToEdgeActive && y > 0) {
1636
+ y += webViewTopInset;
1637
+ }
1638
+ int width = (widthParam != null && widthParam > 0)
1639
+ ? (int) (widthParam * pixelRatio)
1640
+ : 0;
1641
+ int height = (heightParam != null && heightParam > 0)
1642
+ ? (int) (heightParam * pixelRatio)
1643
+ : 0;
1644
+
651
1645
  cameraXView.setPreviewSize(x, y, width, height, () -> {
652
1646
  // Return the actual preview bounds after layout operations are complete
653
1647
  int[] bounds = cameraXView.getCurrentPreviewBounds();
@@ -659,4 +1653,116 @@ public class CameraPreview
659
1653
  call.resolve(ret);
660
1654
  });
661
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
+ }
662
1768
  }