@capgo/camera-preview 7.23.10 → 7.23.12

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.
@@ -50,2347 +50,1894 @@ import java.util.Objects;
50
50
  import org.json.JSONObject;
51
51
 
52
52
  @CapacitorPlugin(
53
- name = "CameraPreview",
54
- permissions = {
55
- @Permission(
56
- strings = { CAMERA, RECORD_AUDIO },
57
- alias = CameraPreview.CAMERA_WITH_AUDIO_PERMISSION_ALIAS
58
- ),
59
- @Permission(
60
- strings = { CAMERA },
61
- alias = CameraPreview.CAMERA_ONLY_PERMISSION_ALIAS
62
- ),
63
- @Permission(
64
- strings = {
65
- Manifest.permission.ACCESS_COARSE_LOCATION,
66
- Manifest.permission.ACCESS_FINE_LOCATION,
67
- },
68
- alias = CameraPreview.CAMERA_WITH_LOCATION_PERMISSION_ALIAS
69
- ),
70
- @Permission(
71
- strings = { RECORD_AUDIO },
72
- alias = CameraPreview.MICROPHONE_ONLY_PERMISSION_ALIAS
73
- ),
74
- }
53
+ name = "CameraPreview",
54
+ permissions = {
55
+ @Permission(strings = { CAMERA, RECORD_AUDIO }, alias = CameraPreview.CAMERA_WITH_AUDIO_PERMISSION_ALIAS),
56
+ @Permission(strings = { CAMERA }, alias = CameraPreview.CAMERA_ONLY_PERMISSION_ALIAS),
57
+ @Permission(
58
+ strings = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION },
59
+ alias = CameraPreview.CAMERA_WITH_LOCATION_PERMISSION_ALIAS
60
+ ),
61
+ @Permission(strings = { RECORD_AUDIO }, alias = CameraPreview.MICROPHONE_ONLY_PERMISSION_ALIAS)
62
+ }
75
63
  )
76
- public class CameraPreview
77
- extends Plugin
78
- implements CameraXView.CameraXViewListener {
79
-
80
- @Override
81
- protected void handleOnPause() {
82
- super.handleOnPause();
83
- if (cameraXView != null && cameraXView.isRunning()) {
84
- // Store the current configuration before stopping
85
- lastSessionConfig = cameraXView.getSessionConfig();
86
- cameraXView.stopSession();
87
- }
88
- }
89
-
90
- @Override
91
- protected void handleOnResume() {
92
- super.handleOnResume();
93
- if (lastSessionConfig != null) {
94
- // Recreate camera with last known configuration
95
- if (cameraXView == null) {
96
- cameraXView = new CameraXView(getContext(), getBridge().getWebView());
97
- cameraXView.setListener(this);
98
- }
99
- cameraXView.startSession(lastSessionConfig);
100
- }
101
- }
102
-
103
- @Override
104
- protected void handleOnDestroy() {
105
- super.handleOnDestroy();
106
- if (cameraXView != null) {
107
- cameraXView.stopSession();
108
- cameraXView = null;
109
- }
110
- lastSessionConfig = null;
111
- }
112
-
113
- private CameraSessionConfiguration lastSessionConfig;
114
-
115
- private static final String TAG = "CameraPreview CameraXView";
116
-
117
- static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
118
- static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
119
- static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS =
120
- "cameraWithLocation";
121
- static final String MICROPHONE_ONLY_PERMISSION_ALIAS = "microphoneOnly";
122
-
123
- private String captureCallbackId = "";
124
- private String sampleCallbackId = "";
125
- private String cameraStartCallbackId = "";
126
- private final Object pendingStartLock = new Object();
127
- private PluginCall pendingStartCall;
128
- private int previousOrientationRequest =
129
- ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
130
- private CameraXView cameraXView;
131
- private View rotationOverlay;
132
- private FusedLocationProviderClient fusedLocationClient;
133
- private Location lastLocation;
134
- private OrientationEventListener orientationListener;
135
- private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
136
- private String lastOrientationStr = "unknown";
137
- private boolean lastDisableAudio = true;
138
- private Drawable originalWindowBackground;
139
- private boolean isCameraPermissionDialogShowing = false;
140
-
141
- @PluginMethod
142
- public void getExposureModes(PluginCall call) {
143
- if (cameraXView == null || !cameraXView.isRunning()) {
144
- call.reject("Camera is not running");
145
- return;
146
- }
147
- JSArray arr = new JSArray();
148
- for (String m : cameraXView.getExposureModes()) arr.put(m);
149
- JSObject ret = new JSObject();
150
- ret.put("modes", arr);
151
- call.resolve(ret);
152
- }
153
-
154
- @PluginMethod
155
- public void getExposureMode(PluginCall call) {
156
- if (cameraXView == null || !cameraXView.isRunning()) {
157
- call.reject("Camera is not running");
158
- return;
159
- }
160
- JSObject ret = new JSObject();
161
- ret.put("mode", cameraXView.getExposureMode());
162
- call.resolve(ret);
163
- }
164
-
165
- @PluginMethod
166
- public void setExposureMode(PluginCall call) {
167
- if (cameraXView == null || !cameraXView.isRunning()) {
168
- call.reject("Camera is not running");
169
- return;
170
- }
171
- String mode = call.getString("mode");
172
- if (mode == null || mode.isEmpty()) {
173
- call.reject("mode parameter is required");
174
- return;
175
- }
176
- try {
177
- cameraXView.setExposureMode(mode);
178
- call.resolve();
179
- } catch (Exception e) {
180
- call.reject("Failed to set exposure mode: " + e.getMessage());
181
- }
182
- }
183
-
184
- @PluginMethod
185
- public void getExposureCompensationRange(PluginCall call) {
186
- if (cameraXView == null || !cameraXView.isRunning()) {
187
- call.reject("Camera is not running");
188
- return;
189
- }
190
- try {
191
- float[] range = cameraXView.getExposureCompensationRange();
192
- JSObject ret = new JSObject();
193
- ret.put("min", range[0]);
194
- ret.put("max", range[1]);
195
- ret.put("step", range.length > 2 ? range[2] : 0.1);
196
- call.resolve(ret);
197
- } catch (Exception e) {
198
- call.reject(
199
- "Failed to get exposure compensation range: " + e.getMessage()
200
- );
201
- }
202
- }
203
-
204
- @PluginMethod
205
- public void getExposureCompensation(PluginCall call) {
206
- if (cameraXView == null || !cameraXView.isRunning()) {
207
- call.reject("Camera is not running");
208
- return;
209
- }
210
- try {
211
- float value = cameraXView.getExposureCompensation();
212
- JSObject ret = new JSObject();
213
- ret.put("value", value);
214
- call.resolve(ret);
215
- } catch (Exception e) {
216
- call.reject("Failed to get exposure compensation: " + e.getMessage());
217
- }
218
- }
219
-
220
- @PluginMethod
221
- public void setExposureCompensation(PluginCall call) {
222
- if (cameraXView == null || !cameraXView.isRunning()) {
223
- call.reject("Camera is not running");
224
- return;
225
- }
226
- Float value = call.getFloat("value");
227
- if (value == null) {
228
- call.reject("value parameter is required");
229
- return;
230
- }
231
- try {
232
- cameraXView.setExposureCompensation(value);
233
- call.resolve();
234
- } catch (Exception e) {
235
- call.reject("Failed to set exposure compensation: " + e.getMessage());
236
- }
237
- }
238
-
239
- @PluginMethod
240
- public void getOrientation(PluginCall call) {
241
- String o = getDeviceOrientationString();
242
- JSObject ret = new JSObject();
243
- ret.put("orientation", o);
244
- call.resolve(ret);
245
- }
246
-
247
- @PluginMethod
248
- public void start(PluginCall call) {
249
- // Prevent starting while an existing view is still active or stopping
250
- if (cameraXView != null) {
251
- try {
252
- if (cameraXView.isRunning() && !cameraXView.isStopping()) {
253
- call.reject("Camera is already running");
254
- return;
255
- }
256
- if (cameraXView.isStopping() || cameraXView.isBusy()) {
257
- if (enqueuePendingStart(call)) {
258
- Log.d(
259
- TAG,
260
- "start: Camera busy; queued start request until stop completes"
261
- );
262
- return;
263
- }
264
- call.reject("Camera is busy or stopping. Please retry shortly.");
265
- return;
266
- }
267
- } catch (Exception ignored) {}
268
- }
269
- boolean disableAudio = Boolean.TRUE.equals(
270
- call.getBoolean("disableAudio", true)
271
- );
272
- String permissionAlias = disableAudio
273
- ? CAMERA_ONLY_PERMISSION_ALIAS
274
- : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
275
-
276
- if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
277
- startCamera(call);
278
- } else {
279
- requestPermissionForAlias(
280
- permissionAlias,
281
- call,
282
- "handleCameraPermissionResult"
283
- );
284
- }
285
- }
286
-
287
- private boolean enqueuePendingStart(PluginCall call) {
288
- synchronized (pendingStartLock) {
289
- if (pendingStartCall == null) {
290
- pendingStartCall = call;
291
- return true;
292
- }
293
- }
294
- return false;
295
- }
296
-
297
- @PluginMethod
298
- public void flip(PluginCall call) {
299
- if (cameraXView == null || !cameraXView.isRunning()) {
300
- call.reject("Camera is not running");
301
- return;
302
- }
303
- cameraXView.flipCamera();
304
- call.resolve();
305
- }
306
-
307
- @SuppressLint("MissingPermission")
308
- @PluginMethod
309
- public void capture(final PluginCall call) {
310
- if (cameraXView == null || !cameraXView.isRunning()) {
311
- call.reject("Camera is not running");
312
- return;
313
- }
314
-
315
- final boolean withExifLocation = Boolean.TRUE.equals(
316
- call.getBoolean("withExifLocation", false)
317
- );
318
-
319
- if (withExifLocation) {
320
- if (
321
- getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) !=
322
- PermissionState.GRANTED
323
- ) {
324
- requestPermissionForAlias(
325
- CAMERA_WITH_LOCATION_PERMISSION_ALIAS,
326
- call,
327
- "captureWithLocationPermission"
328
- );
329
- } else {
330
- getLocationAndCapture(call);
331
- }
332
- } else {
333
- captureWithoutLocation(call);
334
- }
335
- }
336
-
337
- @SuppressLint("MissingPermission")
338
- @PermissionCallback
339
- private void captureWithLocationPermission(PluginCall call) {
340
- if (
341
- getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) ==
342
- PermissionState.GRANTED
343
- ) {
344
- if (
345
- ActivityCompat.checkSelfPermission(
346
- getContext(),
347
- Manifest.permission.ACCESS_FINE_LOCATION
348
- ) !=
349
- PackageManager.PERMISSION_GRANTED ||
350
- ActivityCompat.checkSelfPermission(
351
- getContext(),
352
- Manifest.permission.ACCESS_COARSE_LOCATION
353
- ) !=
354
- PackageManager.PERMISSION_GRANTED
355
- ) {
356
- return;
357
- }
358
- getLocationAndCapture(call);
359
- } else {
360
- Logger.warn(
361
- "Location permission denied. Capturing photo without location data."
362
- );
363
- captureWithoutLocation(call);
364
- }
365
- }
366
-
367
- @RequiresPermission(
368
- allOf = {
369
- Manifest.permission.ACCESS_FINE_LOCATION,
370
- Manifest.permission.ACCESS_COARSE_LOCATION,
371
- }
372
- )
373
- private void getLocationAndCapture(PluginCall call) {
374
- if (fusedLocationClient == null) {
375
- fusedLocationClient = LocationServices.getFusedLocationProviderClient(
376
- getContext()
377
- );
378
- }
379
- fusedLocationClient
380
- .getLastLocation()
381
- .addOnSuccessListener(getActivity(), location -> {
382
- lastLocation = location;
383
- proceedWithCapture(call, lastLocation);
384
- })
385
- .addOnFailureListener(e -> {
386
- Logger.error("Failed to get location: " + e.getMessage());
387
- proceedWithCapture(call, null);
388
- });
389
- }
390
-
391
- private void captureWithoutLocation(PluginCall call) {
392
- proceedWithCapture(call, null);
393
- }
394
-
395
- private void proceedWithCapture(PluginCall call, Location location) {
396
- bridge.saveCall(call);
397
- captureCallbackId = call.getCallbackId();
398
-
399
- Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
400
- final boolean saveToGallery = Boolean.TRUE.equals(
401
- call.getBoolean("saveToGallery")
402
- );
403
- Integer width = call.getInt("width");
404
- Integer height = call.getInt("height");
405
- final boolean embedTimestamp = Boolean.TRUE.equals(
406
- call.getBoolean("embedTimestamp")
407
- );
408
- final boolean embedLocation = Boolean.TRUE.equals(
409
- call.getBoolean("embedLocation")
410
- );
411
-
412
- cameraXView.capturePhoto(
413
- quality,
414
- saveToGallery,
415
- width,
416
- height,
417
- location,
418
- embedTimestamp,
419
- embedLocation
420
- );
421
- }
422
-
423
- @PluginMethod
424
- public void captureSample(PluginCall call) {
425
- if (cameraXView == null || !cameraXView.isRunning()) {
426
- call.reject("Camera is not running");
427
- return;
428
- }
429
- bridge.saveCall(call);
430
- sampleCallbackId = call.getCallbackId();
431
- Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
432
- cameraXView.captureSample(quality);
433
- }
434
-
435
- @PluginMethod
436
- public void stop(final PluginCall call) {
437
- bridge
438
- .getActivity()
439
- .runOnUiThread(() -> {
440
- getBridge()
441
- .getActivity()
442
- .setRequestedOrientation(previousOrientationRequest);
443
-
444
- // Disable and clear orientation listener
445
- if (orientationListener != null) {
446
- orientationListener.disable();
447
- orientationListener = null;
448
- lastOrientation = Configuration.ORIENTATION_UNDEFINED;
64
+ public class CameraPreview extends Plugin implements CameraXView.CameraXViewListener {
65
+
66
+ @Override
67
+ protected void handleOnPause() {
68
+ super.handleOnPause();
69
+ if (cameraXView != null && cameraXView.isRunning()) {
70
+ // Store the current configuration before stopping
71
+ lastSessionConfig = cameraXView.getSessionConfig();
72
+ cameraXView.stopSession();
449
73
  }
74
+ }
450
75
 
451
- // Remove any rotation overlay if present
452
- if (rotationOverlay != null && rotationOverlay.getParent() != null) {
453
- ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
454
- rotationOverlay = null;
76
+ @Override
77
+ protected void handleOnResume() {
78
+ super.handleOnResume();
79
+ if (lastSessionConfig != null) {
80
+ // Recreate camera with last known configuration
81
+ if (cameraXView == null) {
82
+ cameraXView = new CameraXView(getContext(), getBridge().getWebView());
83
+ cameraXView.setListener(this);
84
+ }
85
+ cameraXView.startSession(lastSessionConfig);
455
86
  }
87
+ }
456
88
 
89
+ @Override
90
+ protected void handleOnDestroy() {
91
+ super.handleOnDestroy();
457
92
  if (cameraXView != null) {
458
- cameraXView.stopSession();
459
- // Only drop the reference if no deferred stop is pending
460
- if (!cameraXView.isStopDeferred()) {
93
+ cameraXView.stopSession();
461
94
  cameraXView = null;
462
- }
463
95
  }
464
- // Manual stops should not trigger automatic resume with stale config
465
96
  lastSessionConfig = null;
466
- // Restore original window background if modified earlier
467
- if (originalWindowBackground != null) {
468
- try {
469
- getBridge()
470
- .getActivity()
471
- .getWindow()
472
- .setBackgroundDrawable(originalWindowBackground);
473
- } catch (Exception ignored) {}
474
- originalWindowBackground = null;
97
+ }
98
+
99
+ private CameraSessionConfiguration lastSessionConfig;
100
+
101
+ private static final String TAG = "CameraPreview CameraXView";
102
+
103
+ static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
104
+ static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
105
+ static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS = "cameraWithLocation";
106
+ static final String MICROPHONE_ONLY_PERMISSION_ALIAS = "microphoneOnly";
107
+
108
+ private String captureCallbackId = "";
109
+ private String sampleCallbackId = "";
110
+ private String cameraStartCallbackId = "";
111
+ private final Object pendingStartLock = new Object();
112
+ private PluginCall pendingStartCall;
113
+ private int previousOrientationRequest = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
114
+ private CameraXView cameraXView;
115
+ private View rotationOverlay;
116
+ private FusedLocationProviderClient fusedLocationClient;
117
+ private Location lastLocation;
118
+ private OrientationEventListener orientationListener;
119
+ private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
120
+ private String lastOrientationStr = "unknown";
121
+ private boolean lastDisableAudio = true;
122
+ private Drawable originalWindowBackground;
123
+ private boolean isCameraPermissionDialogShowing = false;
124
+
125
+ @PluginMethod
126
+ public void getExposureModes(PluginCall call) {
127
+ if (cameraXView == null || !cameraXView.isRunning()) {
128
+ call.reject("Camera is not running");
129
+ return;
130
+ }
131
+ JSArray arr = new JSArray();
132
+ for (String m : cameraXView.getExposureModes()) arr.put(m);
133
+ JSObject ret = new JSObject();
134
+ ret.put("modes", arr);
135
+ call.resolve(ret);
136
+ }
137
+
138
+ @PluginMethod
139
+ public void getExposureMode(PluginCall call) {
140
+ if (cameraXView == null || !cameraXView.isRunning()) {
141
+ call.reject("Camera is not running");
142
+ return;
143
+ }
144
+ JSObject ret = new JSObject();
145
+ ret.put("mode", cameraXView.getExposureMode());
146
+ call.resolve(ret);
147
+ }
148
+
149
+ @PluginMethod
150
+ public void setExposureMode(PluginCall call) {
151
+ if (cameraXView == null || !cameraXView.isRunning()) {
152
+ call.reject("Camera is not running");
153
+ return;
154
+ }
155
+ String mode = call.getString("mode");
156
+ if (mode == null || mode.isEmpty()) {
157
+ call.reject("mode parameter is required");
158
+ return;
475
159
  }
476
- call.resolve();
477
- });
478
- }
479
-
480
- @PluginMethod
481
- public void getSupportedFlashModes(PluginCall call) {
482
- if (cameraXView == null || !cameraXView.isRunning()) {
483
- call.reject("Camera is not running");
484
- return;
485
- }
486
- List<String> supportedFlashModes = cameraXView.getSupportedFlashModes();
487
- JSArray jsonFlashModes = new JSArray();
488
- for (String mode : supportedFlashModes) {
489
- jsonFlashModes.put(mode);
490
- }
491
- JSObject jsObject = new JSObject();
492
- jsObject.put("result", jsonFlashModes);
493
- call.resolve(jsObject);
494
- }
495
-
496
- @PluginMethod
497
- public void setFlashMode(PluginCall call) {
498
- String flashMode = call.getString("flashMode");
499
- if (flashMode == null || flashMode.isEmpty()) {
500
- call.reject("flashMode required parameter is missing");
501
- return;
502
- }
503
- cameraXView.setFlashMode(flashMode);
504
- call.resolve();
505
- }
506
-
507
- @PluginMethod
508
- public void getAvailableDevices(PluginCall call) {
509
- List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
510
- getContext()
511
- );
512
- JSArray devicesArray = new JSArray();
513
- for (CameraDevice device : devices) {
514
- JSObject deviceJson = new JSObject();
515
- deviceJson.put("deviceId", device.getDeviceId());
516
- deviceJson.put("label", device.getLabel());
517
- deviceJson.put("position", device.getPosition());
518
- JSArray lensesArray = new JSArray();
519
- for (app.capgo.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
520
- JSObject lensJson = new JSObject();
521
- lensJson.put("focalLength", lens.getFocalLength());
522
- lensJson.put("deviceType", lens.getDeviceType());
523
- lensJson.put("baseZoomRatio", lens.getBaseZoomRatio());
524
- lensJson.put("digitalZoom", lens.getDigitalZoom());
525
- lensesArray.put(lensJson);
526
- }
527
- deviceJson.put("lenses", lensesArray);
528
- deviceJson.put("minZoom", device.getMinZoom());
529
- deviceJson.put("maxZoom", device.getMaxZoom());
530
- devicesArray.put(deviceJson);
531
- }
532
- JSObject result = new JSObject();
533
- result.put("devices", devicesArray);
534
- call.resolve(result);
535
- }
536
-
537
- @PluginMethod
538
- public void getZoom(PluginCall call) {
539
- if (cameraXView == null || !cameraXView.isRunning()) {
540
- call.reject("Camera is not running");
541
- return;
542
- }
543
- ZoomFactors zoomFactors = cameraXView.getZoomFactors();
544
- JSObject result = new JSObject();
545
- result.put("min", zoomFactors.getMin());
546
- result.put("max", zoomFactors.getMax());
547
- result.put("current", zoomFactors.getCurrent());
548
- call.resolve(result);
549
- }
550
-
551
- @PluginMethod
552
- public void getZoomButtonValues(PluginCall call) {
553
- if (cameraXView == null || !cameraXView.isRunning()) {
554
- call.reject("Camera is not running");
555
- return;
556
- }
557
- // Build a sorted set to dedupe and order ascending
558
- java.util.Set<Double> sorted = new java.util.TreeSet<>();
559
- sorted.add(1.0);
560
- sorted.add(2.0);
561
-
562
- // Try to detect ultra-wide to include its min zoom (often 0.5)
563
- try {
564
- List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
565
- getContext()
566
- );
567
- ZoomFactors zoomFactors = cameraXView.getZoomFactors();
568
- boolean hasUltraWide = false;
569
- boolean hasTelephoto = false;
570
- float minUltra = 0.5f;
571
-
572
- for (CameraDevice device : devices) {
573
- for (app.capgo.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
574
- if ("ultraWide".equals(lens.getDeviceType())) {
575
- hasUltraWide = true;
576
- // Use overall minZoom for that device as the button value to represent UW
577
- minUltra = Math.max(minUltra, zoomFactors.getMin());
578
- } else if ("telephoto".equals(lens.getDeviceType())) {
579
- hasTelephoto = true;
580
- }
581
- }
582
- }
583
- if (hasUltraWide) {
584
- sorted.add((double) minUltra);
585
- }
586
- if (hasTelephoto) {
587
- sorted.add(3.0);
588
- }
589
- } catch (Exception ignored) {
590
- // Ignore and keep defaults
591
- }
592
-
593
- JSObject result = new JSObject();
594
- JSArray values = new JSArray();
595
- for (Double v : sorted) {
596
- values.put(v);
597
- }
598
- result.put("values", values);
599
- call.resolve(result);
600
- }
601
-
602
- @PluginMethod
603
- public void setZoom(PluginCall call) {
604
- if (cameraXView == null || !cameraXView.isRunning()) {
605
- call.reject("Camera is not running");
606
- return;
607
- }
608
- Float level = call.getFloat("level");
609
- if (level == null) {
610
- call.reject("level parameter is required");
611
- return;
612
- }
613
- try {
614
- cameraXView.setZoom(level);
615
- call.resolve();
616
- } catch (Exception e) {
617
- call.reject("Failed to set zoom: " + e.getMessage());
618
- }
619
- }
620
-
621
- @PluginMethod
622
- public void setFocus(PluginCall call) {
623
- if (cameraXView == null || !cameraXView.isRunning()) {
624
- call.reject("Camera is not running");
625
- return;
626
- }
627
- Float x = call.getFloat("x");
628
- Float y = call.getFloat("y");
629
- if (x == null || y == null) {
630
- call.reject("x and y parameters are required");
631
- return;
632
- }
633
- // Reject if values are outside 0-1 range
634
- if (x < 0f || x > 1f || y < 0f || y > 1f) {
635
- call.reject("Focus coordinates must be between 0 and 1");
636
- return;
637
- }
638
-
639
- getActivity()
640
- .runOnUiThread(() -> {
641
160
  try {
642
- cameraXView.setFocus(x, y);
643
- call.resolve();
161
+ cameraXView.setExposureMode(mode);
162
+ call.resolve();
644
163
  } catch (Exception e) {
645
- call.reject("Failed to set focus: " + e.getMessage());
646
- }
647
- });
648
- }
649
-
650
- @PluginMethod
651
- public void setDeviceId(PluginCall call) {
652
- String deviceId = call.getString("deviceId");
653
- if (deviceId == null || deviceId.isEmpty()) {
654
- call.reject("deviceId parameter is required");
655
- return;
656
- }
657
- if (cameraXView == null || !cameraXView.isRunning()) {
658
- call.reject("Camera is not running");
659
- return;
660
- }
661
- cameraXView.switchToDevice(deviceId);
662
- call.resolve();
663
- }
664
-
665
- @PluginMethod
666
- public void getSupportedPictureSizes(final PluginCall call) {
667
- JSArray supportedPictureSizesResult = new JSArray();
668
- List<Size> rearSizes = CameraXView.getSupportedPictureSizes("rear");
669
- JSObject rear = new JSObject();
670
- rear.put("facing", "rear");
671
- JSArray rearSizesJs = new JSArray();
672
- for (Size size : rearSizes) {
673
- JSObject sizeJs = new JSObject();
674
- sizeJs.put("width", size.getWidth());
675
- sizeJs.put("height", size.getHeight());
676
- rearSizesJs.put(sizeJs);
677
- }
678
- rear.put("supportedPictureSizes", rearSizesJs);
679
- supportedPictureSizesResult.put(rear);
680
-
681
- List<Size> frontSizes = CameraXView.getSupportedPictureSizes("front");
682
- JSObject front = new JSObject();
683
- front.put("facing", "front");
684
- JSArray frontSizesJs = new JSArray();
685
- for (Size size : frontSizes) {
686
- JSObject sizeJs = new JSObject();
687
- sizeJs.put("width", size.getWidth());
688
- sizeJs.put("height", size.getHeight());
689
- frontSizesJs.put(sizeJs);
690
- }
691
- front.put("supportedPictureSizes", frontSizesJs);
692
- supportedPictureSizesResult.put(front);
693
-
694
- JSObject ret = new JSObject();
695
- ret.put("supportedPictureSizes", supportedPictureSizesResult);
696
- call.resolve(ret);
697
- }
698
-
699
- @PluginMethod
700
- public void setOpacity(PluginCall call) {
701
- if (cameraXView == null || !cameraXView.isRunning()) {
702
- call.reject("Camera is not running");
703
- return;
704
- }
705
- Float opacity = call.getFloat("opacity", 1.0f);
706
- //noinspection DataFlowIssue
707
- cameraXView.setOpacity(opacity);
708
- call.resolve();
709
- }
710
-
711
- @PluginMethod
712
- public void getHorizontalFov(PluginCall call) {
713
- // CameraX does not provide a simple way to get FoV.
714
- // This would require Camera2 interop to access camera characteristics.
715
- // Returning a default/estimated value.
716
- JSObject ret = new JSObject();
717
- ret.put("result", 60.0); // A common default FoV
718
- call.resolve(ret);
719
- }
720
-
721
- @PluginMethod
722
- public void getDeviceId(PluginCall call) {
723
- if (cameraXView == null || !cameraXView.isRunning()) {
724
- call.reject("Camera is not running");
725
- return;
726
- }
727
- JSObject ret = new JSObject();
728
- ret.put("deviceId", cameraXView.getCurrentDeviceId());
729
- call.resolve(ret);
730
- }
731
-
732
- @PluginMethod
733
- public void getFlashMode(PluginCall call) {
734
- if (cameraXView == null || !cameraXView.isRunning()) {
735
- call.reject("Camera is not running");
736
- return;
737
- }
738
- JSObject ret = new JSObject();
739
- ret.put("flashMode", cameraXView.getFlashMode());
740
- call.resolve(ret);
741
- }
742
-
743
- @PluginMethod
744
- public void isRunning(PluginCall call) {
745
- boolean running = cameraXView != null && cameraXView.isRunning();
746
- JSObject jsObject = new JSObject();
747
- jsObject.put("isRunning", running);
748
- call.resolve(jsObject);
749
- }
750
-
751
- private void showCameraPermissionDialog(
752
- String title,
753
- String message,
754
- String openSettingsText,
755
- String cancelText,
756
- Runnable completion
757
- ) {
758
- Activity activity = getActivity();
759
- if (activity == null) {
760
- if (completion != null) {
761
- completion.run();
762
- }
763
- return;
764
- }
765
-
766
- activity.runOnUiThread(() -> {
767
- if (activity.isFinishing()) {
768
- if (completion != null) {
769
- completion.run();
770
- }
771
- return;
772
- }
773
-
774
- if (isCameraPermissionDialogShowing) {
775
- if (completion != null) {
776
- completion.run();
777
- }
778
- return;
779
- }
780
-
781
- AlertDialog dialog = new AlertDialog.Builder(activity)
782
- .setTitle(title)
783
- .setMessage(message)
784
- .setNegativeButton(cancelText, (d, which) -> {
785
- d.dismiss();
786
- isCameraPermissionDialogShowing = false;
787
- })
788
- .setPositiveButton(openSettingsText, (d, which) -> {
789
- Intent intent = new Intent(
790
- Settings.ACTION_APPLICATION_DETAILS_SETTINGS
791
- );
792
- Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
793
- intent.setData(uri);
794
- activity.startActivity(intent);
795
- isCameraPermissionDialogShowing = false;
796
- })
797
- .setOnDismissListener(d -> isCameraPermissionDialogShowing = false)
798
- .create();
799
-
800
- isCameraPermissionDialogShowing = true;
801
- dialog.show();
802
- if (completion != null) {
803
- completion.run();
804
- }
805
- });
806
- }
807
-
808
- private String mapPermissionState(PermissionState state) {
809
- if (state == null) {
810
- return PermissionState.PROMPT.toString();
811
- }
812
-
813
- return state.toString();
814
- }
815
-
816
- @PluginMethod
817
- public void checkPermissions(PluginCall call) {
818
- boolean disableAudio = call.getBoolean("disableAudio") != null
819
- ? Boolean.TRUE.equals(call.getBoolean("disableAudio"))
820
- : true;
821
-
822
- PermissionState cameraState = getPermissionState(
823
- CAMERA_ONLY_PERMISSION_ALIAS
824
- );
825
-
826
- JSObject result = new JSObject();
827
- result.put("camera", mapPermissionState(cameraState));
828
-
829
- if (!disableAudio) {
830
- PermissionState audioState = getPermissionState(
831
- MICROPHONE_ONLY_PERMISSION_ALIAS
832
- );
833
- result.put("microphone", mapPermissionState(audioState));
834
- }
835
-
836
- call.resolve(result);
837
- }
838
-
839
- @Override
840
- @PluginMethod
841
- public void requestPermissions(PluginCall call) {
842
- Boolean disableAudioOption = call.getBoolean("disableAudio");
843
- boolean disableAudio = disableAudioOption == null
844
- ? true
845
- : Boolean.TRUE.equals(disableAudioOption);
846
- this.lastDisableAudio = disableAudio;
847
-
848
- String permissionAlias = disableAudio
849
- ? CAMERA_ONLY_PERMISSION_ALIAS
850
- : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
851
-
852
- boolean cameraGranted = PermissionState.GRANTED.equals(
853
- getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
854
- );
855
- boolean audioGranted =
856
- disableAudio ||
857
- PermissionState.GRANTED.equals(
858
- getPermissionState(MICROPHONE_ONLY_PERMISSION_ALIAS)
859
- );
860
-
861
- if (cameraGranted && audioGranted) {
862
- JSObject result = new JSObject();
863
- result.put("camera", mapPermissionState(PermissionState.GRANTED));
864
- if (!disableAudio) {
865
- result.put("microphone", mapPermissionState(PermissionState.GRANTED));
866
- }
867
- call.resolve(result);
868
- return;
869
- }
870
-
871
- requestPermissionForAlias(
872
- permissionAlias,
873
- call,
874
- "handleRequestPermissionsResult"
875
- );
876
- }
877
-
878
- @PermissionCallback
879
- private void handleRequestPermissionsResult(PluginCall call) {
880
- Boolean disableAudioOption = call.getBoolean("disableAudio");
881
- boolean disableAudio = disableAudioOption == null
882
- ? true
883
- : Boolean.TRUE.equals(disableAudioOption);
884
- this.lastDisableAudio = disableAudio;
885
-
886
- PermissionState cameraState = getPermissionState(
887
- CAMERA_ONLY_PERMISSION_ALIAS
888
- );
889
- JSObject result = new JSObject();
890
- result.put("camera", mapPermissionState(cameraState));
891
-
892
- if (!disableAudio) {
893
- PermissionState audioState = getPermissionState(
894
- CAMERA_WITH_AUDIO_PERMISSION_ALIAS
895
- );
896
- result.put("microphone", mapPermissionState(audioState));
897
- }
898
-
899
- boolean showSettingsAlert = call.getBoolean("showSettingsAlert") != null
900
- ? Boolean.TRUE.equals(call.getBoolean("showSettingsAlert"))
901
- : false;
902
-
903
- String cameraStateString = result.getString("camera");
904
- boolean cameraNeedsSettings =
905
- "denied".equals(cameraStateString) ||
906
- "prompt-with-rationale".equals(cameraStateString);
907
-
908
- boolean microphoneNeedsSettings = false;
909
- if (result.has("microphone")) {
910
- String micStateString = result.getString("microphone");
911
- microphoneNeedsSettings =
912
- "denied".equals(micStateString) ||
913
- "prompt-with-rationale".equals(micStateString);
914
- }
915
-
916
- boolean shouldShowAlert =
917
- showSettingsAlert && (cameraNeedsSettings || microphoneNeedsSettings);
918
-
919
- if (shouldShowAlert) {
920
- Activity activity = getActivity();
921
- if (activity == null) {
922
- call.resolve(result);
923
- return;
924
- }
925
-
926
- String title = call.getString("title", "Camera Permission Needed");
927
- String message = call.getString(
928
- "message",
929
- "Enable camera access in Settings to use the preview."
930
- );
931
- String openSettingsText = call.getString(
932
- "openSettingsButtonTitle",
933
- "Open Settings"
934
- );
935
- String cancelText = call.getString(
936
- "cancelButtonTitle",
937
- activity.getString(android.R.string.cancel)
938
- );
939
-
940
- showCameraPermissionDialog(
941
- title,
942
- message,
943
- openSettingsText,
944
- cancelText,
945
- () -> call.resolve(result)
946
- );
947
- } else {
948
- call.resolve(result);
949
- }
950
- }
951
-
952
- @PermissionCallback
953
- private void handleCameraPermissionResult(PluginCall call) {
954
- if (
955
- PermissionState.GRANTED.equals(
956
- getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
957
- ) ||
958
- PermissionState.GRANTED.equals(
959
- getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS)
960
- )
961
- ) {
962
- startCamera(call);
963
- } else {
964
- call.reject(
965
- "camera permission denied. enable camera access in Settings.",
966
- "cameraPermissionDenied"
967
- );
968
- }
969
- }
970
-
971
- private void startCamera(final PluginCall call) {
972
- String positionParam = call.getString("position");
973
- String originalDeviceId = call.getString("deviceId");
974
- String deviceId = originalDeviceId; // Use a mutable variable
975
-
976
- final String position = (positionParam == null ||
977
- positionParam.isEmpty() ||
978
- "rear".equals(positionParam) ||
979
- "back".equals(positionParam))
980
- ? "back"
981
- : "front";
982
- // Use -1 as default to indicate centering is needed when x/y not provided
983
- final Integer xParam = call.getInt("x");
984
- final Integer yParam = call.getInt("y");
985
- final int x = xParam != null ? xParam : -1;
986
- final int y = yParam != null ? yParam : -1;
987
-
988
- Log.d("CameraPreview", "========================");
989
- Log.d("CameraPreview", "CAMERA POSITION TRACKING START:");
990
- Log.d(
991
- "CameraPreview",
992
- "1. RAW PARAMS - xParam: " + xParam + ", yParam: " + yParam
993
- );
994
- Log.d(
995
- "CameraPreview",
996
- "2. AFTER DEFAULT - x: " +
997
- x +
998
- " (center=" +
999
- (x == -1) +
1000
- "), y: " +
1001
- y +
1002
- " (center=" +
1003
- (y == -1) +
1004
- ")"
1005
- );
1006
- //noinspection DataFlowIssue
1007
- final int width = call.getInt("width", 0);
1008
- //noinspection DataFlowIssue
1009
- final int height = call.getInt("height", 0);
1010
- //noinspection DataFlowIssue
1011
- final int paddingBottom = call.getInt("paddingBottom", 0);
1012
- final boolean toBack = Boolean.TRUE.equals(call.getBoolean("toBack", true));
1013
- final boolean storeToFile = Boolean.TRUE.equals(
1014
- call.getBoolean("storeToFile", false)
1015
- );
1016
- final boolean enableOpacity = Boolean.TRUE.equals(
1017
- call.getBoolean("enableOpacity", false)
1018
- );
1019
- final boolean disableExifHeaderStripping = Boolean.TRUE.equals(
1020
- call.getBoolean("disableExifHeaderStripping", false)
1021
- );
1022
- final boolean lockOrientation = Boolean.TRUE.equals(
1023
- call.getBoolean("lockAndroidOrientation", false)
1024
- );
1025
- final boolean disableAudio = Boolean.TRUE.equals(
1026
- call.getBoolean("disableAudio", true)
1027
- );
1028
- this.lastDisableAudio = disableAudio;
1029
- final String aspectRatio = call.getString("aspectRatio", "4:3");
1030
- final String gridMode = call.getString("gridMode", "none");
1031
- final String positioning = call.getString("positioning", "top");
1032
- //noinspection DataFlowIssue
1033
- final float initialZoomLevel = call.getFloat("initialZoomLevel", 1.0f);
1034
- //noinspection DataFlowIssue
1035
- final boolean disableFocusIndicator = call.getBoolean(
1036
- "disableFocusIndicator",
1037
- false
1038
- );
1039
- final boolean enableVideoMode = Boolean.TRUE.equals(
1040
- call.getBoolean("enableVideoMode", false)
1041
- );
1042
-
1043
- // Check for conflict between aspectRatio and size
1044
- if (
1045
- call.getData().has("aspectRatio") &&
1046
- (call.getData().has("width") || call.getData().has("height"))
1047
- ) {
1048
- call.reject(
1049
- "Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."
1050
- );
1051
- return;
1052
- }
1053
-
1054
- float targetZoom = initialZoomLevel;
1055
- // Check if the selected device is a physical ultra-wide
1056
- if (originalDeviceId != null) {
1057
- List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
1058
- getContext()
1059
- );
1060
- for (CameraDevice device : devices) {
1061
- if (
1062
- originalDeviceId.equals(device.getDeviceId()) && !device.isLogical()
1063
- ) {
1064
- for (LensInfo lens : device.getLenses()) {
1065
- if ("ultraWide".equals(lens.getDeviceType())) {
1066
- Log.d(
1067
- "CameraPreview",
1068
- "Ultra-wide lens selected. Targeting 0.5x zoom on logical camera."
1069
- );
1070
- targetZoom = 0.5f;
1071
- // Force the use of the logical camera by clearing the specific deviceId
1072
- deviceId = null;
1073
- break;
164
+ call.reject("Failed to set exposure mode: " + e.getMessage());
165
+ }
166
+ }
167
+
168
+ @PluginMethod
169
+ public void getExposureCompensationRange(PluginCall call) {
170
+ if (cameraXView == null || !cameraXView.isRunning()) {
171
+ call.reject("Camera is not running");
172
+ return;
173
+ }
174
+ try {
175
+ float[] range = cameraXView.getExposureCompensationRange();
176
+ JSObject ret = new JSObject();
177
+ ret.put("min", range[0]);
178
+ ret.put("max", range[1]);
179
+ ret.put("step", range.length > 2 ? range[2] : 0.1);
180
+ call.resolve(ret);
181
+ } catch (Exception e) {
182
+ call.reject("Failed to get exposure compensation range: " + e.getMessage());
183
+ }
184
+ }
185
+
186
+ @PluginMethod
187
+ public void getExposureCompensation(PluginCall call) {
188
+ if (cameraXView == null || !cameraXView.isRunning()) {
189
+ call.reject("Camera is not running");
190
+ return;
191
+ }
192
+ try {
193
+ float value = cameraXView.getExposureCompensation();
194
+ JSObject ret = new JSObject();
195
+ ret.put("value", value);
196
+ call.resolve(ret);
197
+ } catch (Exception e) {
198
+ call.reject("Failed to get exposure compensation: " + e.getMessage());
199
+ }
200
+ }
201
+
202
+ @PluginMethod
203
+ public void setExposureCompensation(PluginCall call) {
204
+ if (cameraXView == null || !cameraXView.isRunning()) {
205
+ call.reject("Camera is not running");
206
+ return;
207
+ }
208
+ Float value = call.getFloat("value");
209
+ if (value == null) {
210
+ call.reject("value parameter is required");
211
+ return;
212
+ }
213
+ try {
214
+ cameraXView.setExposureCompensation(value);
215
+ call.resolve();
216
+ } catch (Exception e) {
217
+ call.reject("Failed to set exposure compensation: " + e.getMessage());
218
+ }
219
+ }
220
+
221
+ @PluginMethod
222
+ public void getOrientation(PluginCall call) {
223
+ String o = getDeviceOrientationString();
224
+ JSObject ret = new JSObject();
225
+ ret.put("orientation", o);
226
+ call.resolve(ret);
227
+ }
228
+
229
+ @PluginMethod
230
+ public void start(PluginCall call) {
231
+ // Prevent starting while an existing view is still active or stopping
232
+ if (cameraXView != null) {
233
+ try {
234
+ if (cameraXView.isRunning() && !cameraXView.isStopping()) {
235
+ call.reject("Camera is already running");
236
+ return;
237
+ }
238
+ if (cameraXView.isStopping() || cameraXView.isBusy()) {
239
+ if (enqueuePendingStart(call)) {
240
+ Log.d(TAG, "start: Camera busy; queued start request until stop completes");
241
+ return;
242
+ }
243
+ call.reject("Camera is busy or stopping. Please retry shortly.");
244
+ return;
245
+ }
246
+ } catch (Exception ignored) {}
247
+ }
248
+ boolean disableAudio = Boolean.TRUE.equals(call.getBoolean("disableAudio", true));
249
+ String permissionAlias = disableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
250
+
251
+ if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
252
+ startCamera(call);
253
+ } else {
254
+ requestPermissionForAlias(permissionAlias, call, "handleCameraPermissionResult");
255
+ }
256
+ }
257
+
258
+ private boolean enqueuePendingStart(PluginCall call) {
259
+ synchronized (pendingStartLock) {
260
+ if (pendingStartCall == null) {
261
+ pendingStartCall = call;
262
+ return true;
1074
263
  }
1075
- }
1076
- }
1077
- if (deviceId == null) break; // Exit outer loop once we've made our decision
1078
- }
1079
- }
1080
-
1081
- previousOrientationRequest = getBridge()
1082
- .getActivity()
1083
- .getRequestedOrientation();
1084
- cameraXView = new CameraXView(getContext(), getBridge().getWebView());
1085
- cameraXView.setListener(this);
1086
-
1087
- String finalDeviceId = deviceId;
1088
- float finalTargetZoom = targetZoom;
1089
- getBridge()
1090
- .getActivity()
1091
- .runOnUiThread(() -> {
1092
- // Ensure transparent background when preview is behind the WebView (Android 10 fix)
1093
- if (toBack) {
1094
- try {
1095
- if (originalWindowBackground == null) {
1096
- originalWindowBackground = getBridge()
1097
- .getActivity()
1098
- .getWindow()
1099
- .getDecorView()
1100
- .getBackground();
264
+ }
265
+ return false;
266
+ }
267
+
268
+ @PluginMethod
269
+ public void flip(PluginCall call) {
270
+ if (cameraXView == null || !cameraXView.isRunning()) {
271
+ call.reject("Camera is not running");
272
+ return;
273
+ }
274
+ cameraXView.flipCamera();
275
+ call.resolve();
276
+ }
277
+
278
+ @SuppressLint("MissingPermission")
279
+ @PluginMethod
280
+ public void capture(final PluginCall call) {
281
+ if (cameraXView == null || !cameraXView.isRunning()) {
282
+ call.reject("Camera is not running");
283
+ return;
284
+ }
285
+
286
+ final boolean withExifLocation = Boolean.TRUE.equals(call.getBoolean("withExifLocation", false));
287
+
288
+ if (withExifLocation) {
289
+ if (getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) != PermissionState.GRANTED) {
290
+ requestPermissionForAlias(CAMERA_WITH_LOCATION_PERMISSION_ALIAS, call, "captureWithLocationPermission");
291
+ } else {
292
+ getLocationAndCapture(call);
1101
293
  }
1102
- getBridge()
1103
- .getActivity()
1104
- .getWindow()
1105
- .setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1106
- } catch (Exception ignored) {}
1107
- }
1108
- DisplayMetrics metrics = getBridge()
1109
- .getActivity()
1110
- .getResources()
1111
- .getDisplayMetrics();
1112
- if (lockOrientation) {
1113
- getBridge()
294
+ } else {
295
+ captureWithoutLocation(call);
296
+ }
297
+ }
298
+
299
+ @SuppressLint("MissingPermission")
300
+ @PermissionCallback
301
+ private void captureWithLocationPermission(PluginCall call) {
302
+ if (getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) == PermissionState.GRANTED) {
303
+ if (
304
+ ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) !=
305
+ PackageManager.PERMISSION_GRANTED ||
306
+ ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) !=
307
+ PackageManager.PERMISSION_GRANTED
308
+ ) {
309
+ return;
310
+ }
311
+ getLocationAndCapture(call);
312
+ } else {
313
+ Logger.warn("Location permission denied. Capturing photo without location data.");
314
+ captureWithoutLocation(call);
315
+ }
316
+ }
317
+
318
+ @RequiresPermission(allOf = { Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION })
319
+ private void getLocationAndCapture(PluginCall call) {
320
+ if (fusedLocationClient == null) {
321
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(getContext());
322
+ }
323
+ fusedLocationClient
324
+ .getLastLocation()
325
+ .addOnSuccessListener(getActivity(), (location) -> {
326
+ lastLocation = location;
327
+ proceedWithCapture(call, lastLocation);
328
+ })
329
+ .addOnFailureListener((e) -> {
330
+ Logger.error("Failed to get location: " + e.getMessage());
331
+ proceedWithCapture(call, null);
332
+ });
333
+ }
334
+
335
+ private void captureWithoutLocation(PluginCall call) {
336
+ proceedWithCapture(call, null);
337
+ }
338
+
339
+ private void proceedWithCapture(PluginCall call, Location location) {
340
+ bridge.saveCall(call);
341
+ captureCallbackId = call.getCallbackId();
342
+
343
+ Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
344
+ final boolean saveToGallery = Boolean.TRUE.equals(call.getBoolean("saveToGallery"));
345
+ Integer width = call.getInt("width");
346
+ Integer height = call.getInt("height");
347
+ final boolean embedTimestamp = Boolean.TRUE.equals(call.getBoolean("embedTimestamp"));
348
+ final boolean embedLocation = Boolean.TRUE.equals(call.getBoolean("embedLocation"));
349
+
350
+ cameraXView.capturePhoto(quality, saveToGallery, width, height, location, embedTimestamp, embedLocation);
351
+ }
352
+
353
+ @PluginMethod
354
+ public void captureSample(PluginCall call) {
355
+ if (cameraXView == null || !cameraXView.isRunning()) {
356
+ call.reject("Camera is not running");
357
+ return;
358
+ }
359
+ bridge.saveCall(call);
360
+ sampleCallbackId = call.getCallbackId();
361
+ Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
362
+ cameraXView.captureSample(quality);
363
+ }
364
+
365
+ @PluginMethod
366
+ public void stop(final PluginCall call) {
367
+ bridge
1114
368
  .getActivity()
1115
- .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
369
+ .runOnUiThread(() -> {
370
+ getBridge().getActivity().setRequestedOrientation(previousOrientationRequest);
371
+
372
+ // Disable and clear orientation listener
373
+ if (orientationListener != null) {
374
+ orientationListener.disable();
375
+ orientationListener = null;
376
+ lastOrientation = Configuration.ORIENTATION_UNDEFINED;
377
+ }
378
+
379
+ // Remove any rotation overlay if present
380
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
381
+ ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
382
+ rotationOverlay = null;
383
+ }
384
+
385
+ if (cameraXView != null) {
386
+ cameraXView.stopSession();
387
+ // Only drop the reference if no deferred stop is pending
388
+ if (!cameraXView.isStopDeferred()) {
389
+ cameraXView = null;
390
+ }
391
+ }
392
+ // Manual stops should not trigger automatic resume with stale config
393
+ lastSessionConfig = null;
394
+ // Restore original window background if modified earlier
395
+ if (originalWindowBackground != null) {
396
+ try {
397
+ getBridge().getActivity().getWindow().setBackgroundDrawable(originalWindowBackground);
398
+ } catch (Exception ignored) {}
399
+ originalWindowBackground = null;
400
+ }
401
+ call.resolve();
402
+ });
403
+ }
404
+
405
+ @PluginMethod
406
+ public void getSupportedFlashModes(PluginCall call) {
407
+ if (cameraXView == null || !cameraXView.isRunning()) {
408
+ call.reject("Camera is not running");
409
+ return;
1116
410
  }
411
+ List<String> supportedFlashModes = cameraXView.getSupportedFlashModes();
412
+ JSArray jsonFlashModes = new JSArray();
413
+ for (String mode : supportedFlashModes) {
414
+ jsonFlashModes.put(mode);
415
+ }
416
+ JSObject jsObject = new JSObject();
417
+ jsObject.put("result", jsonFlashModes);
418
+ call.resolve(jsObject);
419
+ }
1117
420
 
1118
- // Debug: Let's check all the positioning information
1119
- ViewGroup webViewParent = (ViewGroup) getBridge()
1120
- .getWebView()
1121
- .getParent();
421
+ @PluginMethod
422
+ public void setFlashMode(PluginCall call) {
423
+ String flashMode = call.getString("flashMode");
424
+ if (flashMode == null || flashMode.isEmpty()) {
425
+ call.reject("flashMode required parameter is missing");
426
+ return;
427
+ }
428
+ cameraXView.setFlashMode(flashMode);
429
+ call.resolve();
430
+ }
1122
431
 
1123
- // Get webview position in different coordinate systems
1124
- int[] webViewLocationInWindow = new int[2];
1125
- int[] webViewLocationOnScreen = new int[2];
1126
- getBridge().getWebView().getLocationInWindow(webViewLocationInWindow);
1127
- getBridge().getWebView().getLocationOnScreen(webViewLocationOnScreen);
432
+ @PluginMethod
433
+ public void getAvailableDevices(PluginCall call) {
434
+ List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(getContext());
435
+ JSArray devicesArray = new JSArray();
436
+ for (CameraDevice device : devices) {
437
+ JSObject deviceJson = new JSObject();
438
+ deviceJson.put("deviceId", device.getDeviceId());
439
+ deviceJson.put("label", device.getLabel());
440
+ deviceJson.put("position", device.getPosition());
441
+ JSArray lensesArray = new JSArray();
442
+ for (app.capgo.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
443
+ JSObject lensJson = new JSObject();
444
+ lensJson.put("focalLength", lens.getFocalLength());
445
+ lensJson.put("deviceType", lens.getDeviceType());
446
+ lensJson.put("baseZoomRatio", lens.getBaseZoomRatio());
447
+ lensJson.put("digitalZoom", lens.getDigitalZoom());
448
+ lensesArray.put(lensJson);
449
+ }
450
+ deviceJson.put("lenses", lensesArray);
451
+ deviceJson.put("minZoom", device.getMinZoom());
452
+ deviceJson.put("maxZoom", device.getMaxZoom());
453
+ devicesArray.put(deviceJson);
454
+ }
455
+ JSObject result = new JSObject();
456
+ result.put("devices", devicesArray);
457
+ call.resolve(result);
458
+ }
1128
459
 
1129
- int webViewLeft = getBridge().getWebView().getLeft();
1130
- int webViewTop = getBridge().getWebView().getTop();
460
+ @PluginMethod
461
+ public void getZoom(PluginCall call) {
462
+ if (cameraXView == null || !cameraXView.isRunning()) {
463
+ call.reject("Camera is not running");
464
+ return;
465
+ }
466
+ ZoomFactors zoomFactors = cameraXView.getZoomFactors();
467
+ JSObject result = new JSObject();
468
+ result.put("min", zoomFactors.getMin());
469
+ result.put("max", zoomFactors.getMax());
470
+ result.put("current", zoomFactors.getCurrent());
471
+ call.resolve(result);
472
+ }
1131
473
 
1132
- // Check parent position too
1133
- int[] parentLocationInWindow = new int[2];
1134
- int[] parentLocationOnScreen = new int[2];
1135
- webViewParent.getLocationInWindow(parentLocationInWindow);
1136
- webViewParent.getLocationOnScreen(parentLocationOnScreen);
474
+ @PluginMethod
475
+ public void getZoomButtonValues(PluginCall call) {
476
+ if (cameraXView == null || !cameraXView.isRunning()) {
477
+ call.reject("Camera is not running");
478
+ return;
479
+ }
480
+ // Build a sorted set to dedupe and order ascending
481
+ java.util.Set<Double> sorted = new java.util.TreeSet<>();
482
+ sorted.add(1.0);
483
+ sorted.add(2.0);
1137
484
 
1138
- // Calculate pixel ratio
1139
- float pixelRatio = metrics.density;
485
+ // Try to detect ultra-wide to include its min zoom (often 0.5)
486
+ try {
487
+ List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(getContext());
488
+ ZoomFactors zoomFactors = cameraXView.getZoomFactors();
489
+ boolean hasUltraWide = false;
490
+ boolean hasTelephoto = false;
491
+ float minUltra = 0.5f;
492
+
493
+ for (CameraDevice device : devices) {
494
+ for (app.capgo.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
495
+ if ("ultraWide".equals(lens.getDeviceType())) {
496
+ hasUltraWide = true;
497
+ // Use overall minZoom for that device as the button value to represent UW
498
+ minUltra = Math.max(minUltra, zoomFactors.getMin());
499
+ } else if ("telephoto".equals(lens.getDeviceType())) {
500
+ hasTelephoto = true;
501
+ }
502
+ }
503
+ }
504
+ if (hasUltraWide) {
505
+ sorted.add((double) minUltra);
506
+ }
507
+ if (hasTelephoto) {
508
+ sorted.add(3.0);
509
+ }
510
+ } catch (Exception ignored) {
511
+ // Ignore and keep defaults
512
+ }
1140
513
 
1141
- // The key insight: JavaScript coordinates are relative to the WebView's viewport
1142
- // If the WebView is positioned below the status bar (webViewLocationOnScreen[1] > 0),
1143
- // we need to add that offset when placing native views
1144
- int webViewTopInset = webViewLocationOnScreen[1];
1145
- boolean isEdgeToEdgeActive = webViewLocationOnScreen[1] > 0;
514
+ JSObject result = new JSObject();
515
+ JSArray values = new JSArray();
516
+ for (Double v : sorted) {
517
+ values.put(v);
518
+ }
519
+ result.put("values", values);
520
+ call.resolve(result);
521
+ }
1146
522
 
1147
- // Log all the positioning information for debugging
1148
- Log.d("CameraPreview", "WebView Position Debug:");
1149
- Log.d("CameraPreview", " - webView.getTop(): " + webViewTop);
1150
- Log.d("CameraPreview", " - webView.getLeft(): " + webViewLeft);
1151
- Log.d(
1152
- "CameraPreview",
1153
- " - webView locationInWindow: (" +
1154
- webViewLocationInWindow[0] +
1155
- ", " +
1156
- webViewLocationInWindow[1] +
1157
- ")"
1158
- );
1159
- Log.d(
1160
- "CameraPreview",
1161
- " - webView locationOnScreen: (" +
1162
- webViewLocationOnScreen[0] +
1163
- ", " +
1164
- webViewLocationOnScreen[1] +
1165
- ")"
1166
- );
1167
- Log.d(
1168
- "CameraPreview",
1169
- " - parent locationInWindow: (" +
1170
- parentLocationInWindow[0] +
1171
- ", " +
1172
- parentLocationInWindow[1] +
1173
- ")"
1174
- );
1175
- Log.d(
1176
- "CameraPreview",
1177
- " - parent locationOnScreen: (" +
1178
- parentLocationOnScreen[0] +
1179
- ", " +
1180
- parentLocationOnScreen[1] +
1181
- ")"
1182
- );
523
+ @PluginMethod
524
+ public void setZoom(PluginCall call) {
525
+ if (cameraXView == null || !cameraXView.isRunning()) {
526
+ call.reject("Camera is not running");
527
+ return;
528
+ }
529
+ Float level = call.getFloat("level");
530
+ if (level == null) {
531
+ call.reject("level parameter is required");
532
+ return;
533
+ }
534
+ try {
535
+ cameraXView.setZoom(level);
536
+ call.resolve();
537
+ } catch (Exception e) {
538
+ call.reject("Failed to set zoom: " + e.getMessage());
539
+ }
540
+ }
1183
541
 
1184
- // Check if WebView has margins
1185
- View webView = getBridge().getWebView();
1186
- ViewGroup.LayoutParams webViewLayoutParams = webView.getLayoutParams();
1187
- if (webViewLayoutParams instanceof ViewGroup.MarginLayoutParams) {
1188
- ViewGroup.MarginLayoutParams marginParams =
1189
- (ViewGroup.MarginLayoutParams) webViewLayoutParams;
1190
- Log.d(
1191
- "CameraPreview",
1192
- " - webView margins: left=" +
1193
- marginParams.leftMargin +
1194
- ", top=" +
1195
- marginParams.topMargin +
1196
- ", right=" +
1197
- marginParams.rightMargin +
1198
- ", bottom=" +
1199
- marginParams.bottomMargin
1200
- );
1201
- }
1202
-
1203
- // Check WebView padding
1204
- Log.d(
1205
- "CameraPreview",
1206
- " - webView padding: left=" +
1207
- webView.getPaddingLeft() +
1208
- ", top=" +
1209
- webView.getPaddingTop() +
1210
- ", right=" +
1211
- webView.getPaddingRight() +
1212
- ", bottom=" +
1213
- webView.getPaddingBottom()
1214
- );
542
+ @PluginMethod
543
+ public void setFocus(PluginCall call) {
544
+ if (cameraXView == null || !cameraXView.isRunning()) {
545
+ call.reject("Camera is not running");
546
+ return;
547
+ }
548
+ Float x = call.getFloat("x");
549
+ Float y = call.getFloat("y");
550
+ if (x == null || y == null) {
551
+ call.reject("x and y parameters are required");
552
+ return;
553
+ }
554
+ // Reject if values are outside 0-1 range
555
+ if (x < 0f || x > 1f || y < 0f || y > 1f) {
556
+ call.reject("Focus coordinates must be between 0 and 1");
557
+ return;
558
+ }
1215
559
 
1216
- Log.d("CameraPreview", " - Using webViewTopInset: " + webViewTopInset);
1217
- Log.d("CameraPreview", " - isEdgeToEdgeActive: " + isEdgeToEdgeActive);
560
+ getActivity().runOnUiThread(() -> {
561
+ try {
562
+ cameraXView.setFocus(x, y);
563
+ call.resolve();
564
+ } catch (Exception e) {
565
+ call.reject("Failed to set focus: " + e.getMessage());
566
+ }
567
+ });
568
+ }
1218
569
 
1219
- // Calculate position - center if x or y is -1
1220
- int computedX;
1221
- int computedY;
570
+ @PluginMethod
571
+ public void setDeviceId(PluginCall call) {
572
+ String deviceId = call.getString("deviceId");
573
+ if (deviceId == null || deviceId.isEmpty()) {
574
+ call.reject("deviceId parameter is required");
575
+ return;
576
+ }
577
+ if (cameraXView == null || !cameraXView.isRunning()) {
578
+ call.reject("Camera is not running");
579
+ return;
580
+ }
581
+ cameraXView.switchToDevice(deviceId);
582
+ call.resolve();
583
+ }
1222
584
 
1223
- // Calculate dimensions first
1224
- int computedWidth = width != 0
1225
- ? (int) (width * pixelRatio)
1226
- : getBridge().getWebView().getWidth();
1227
- int computedHeight = height != 0
1228
- ? (int) (height * pixelRatio)
1229
- : getBridge().getWebView().getHeight();
1230
- computedHeight -= (int) (paddingBottom * pixelRatio);
585
+ @PluginMethod
586
+ public void getSupportedPictureSizes(final PluginCall call) {
587
+ JSArray supportedPictureSizesResult = new JSArray();
588
+ List<Size> rearSizes = CameraXView.getSupportedPictureSizes("rear");
589
+ JSObject rear = new JSObject();
590
+ rear.put("facing", "rear");
591
+ JSArray rearSizesJs = new JSArray();
592
+ for (Size size : rearSizes) {
593
+ JSObject sizeJs = new JSObject();
594
+ sizeJs.put("width", size.getWidth());
595
+ sizeJs.put("height", size.getHeight());
596
+ rearSizesJs.put(sizeJs);
597
+ }
598
+ rear.put("supportedPictureSizes", rearSizesJs);
599
+ supportedPictureSizesResult.put(rear);
600
+
601
+ List<Size> frontSizes = CameraXView.getSupportedPictureSizes("front");
602
+ JSObject front = new JSObject();
603
+ front.put("facing", "front");
604
+ JSArray frontSizesJs = new JSArray();
605
+ for (Size size : frontSizes) {
606
+ JSObject sizeJs = new JSObject();
607
+ sizeJs.put("width", size.getWidth());
608
+ sizeJs.put("height", size.getHeight());
609
+ frontSizesJs.put(sizeJs);
610
+ }
611
+ front.put("supportedPictureSizes", frontSizesJs);
612
+ supportedPictureSizesResult.put(front);
1231
613
 
1232
- Log.d("CameraPreview", "========================");
1233
- Log.d("CameraPreview", "POSITIONING CALCULATIONS:");
1234
- Log.d(
1235
- "CameraPreview",
1236
- "1. INPUT - x: " +
1237
- x +
1238
- ", y: " +
1239
- y +
1240
- ", width: " +
1241
- width +
1242
- ", height: " +
1243
- height
1244
- );
1245
- Log.d("CameraPreview", "2. PIXEL RATIO: " + pixelRatio);
1246
- Log.d(
1247
- "CameraPreview",
1248
- "3. SCREEN - width: " +
1249
- metrics.widthPixels +
1250
- ", height: " +
1251
- metrics.heightPixels
1252
- );
1253
- Log.d(
1254
- "CameraPreview",
1255
- "4. WEBVIEW - width: " +
1256
- getBridge().getWebView().getWidth() +
1257
- ", height: " +
1258
- getBridge().getWebView().getHeight()
1259
- );
1260
- Log.d(
1261
- "CameraPreview",
1262
- "5. COMPUTED DIMENSIONS - width: " +
1263
- computedWidth +
1264
- ", height: " +
1265
- computedHeight
1266
- );
614
+ JSObject ret = new JSObject();
615
+ ret.put("supportedPictureSizes", supportedPictureSizesResult);
616
+ call.resolve(ret);
617
+ }
618
+
619
+ @PluginMethod
620
+ public void setOpacity(PluginCall call) {
621
+ if (cameraXView == null || !cameraXView.isRunning()) {
622
+ call.reject("Camera is not running");
623
+ return;
624
+ }
625
+ Float opacity = call.getFloat("opacity", 1.0f);
626
+ //noinspection DataFlowIssue
627
+ cameraXView.setOpacity(opacity);
628
+ call.resolve();
629
+ }
630
+
631
+ @PluginMethod
632
+ public void getHorizontalFov(PluginCall call) {
633
+ // CameraX does not provide a simple way to get FoV.
634
+ // This would require Camera2 interop to access camera characteristics.
635
+ // Returning a default/estimated value.
636
+ JSObject ret = new JSObject();
637
+ ret.put("result", 60.0); // A common default FoV
638
+ call.resolve(ret);
639
+ }
640
+
641
+ @PluginMethod
642
+ public void getDeviceId(PluginCall call) {
643
+ if (cameraXView == null || !cameraXView.isRunning()) {
644
+ call.reject("Camera is not running");
645
+ return;
646
+ }
647
+ JSObject ret = new JSObject();
648
+ ret.put("deviceId", cameraXView.getCurrentDeviceId());
649
+ call.resolve(ret);
650
+ }
651
+
652
+ @PluginMethod
653
+ public void getFlashMode(PluginCall call) {
654
+ if (cameraXView == null || !cameraXView.isRunning()) {
655
+ call.reject("Camera is not running");
656
+ return;
657
+ }
658
+ JSObject ret = new JSObject();
659
+ ret.put("flashMode", cameraXView.getFlashMode());
660
+ call.resolve(ret);
661
+ }
662
+
663
+ @PluginMethod
664
+ public void isRunning(PluginCall call) {
665
+ boolean running = cameraXView != null && cameraXView.isRunning();
666
+ JSObject jsObject = new JSObject();
667
+ jsObject.put("isRunning", running);
668
+ call.resolve(jsObject);
669
+ }
670
+
671
+ private void showCameraPermissionDialog(String title, String message, String openSettingsText, String cancelText, Runnable completion) {
672
+ Activity activity = getActivity();
673
+ if (activity == null) {
674
+ if (completion != null) {
675
+ completion.run();
676
+ }
677
+ return;
678
+ }
679
+
680
+ activity.runOnUiThread(() -> {
681
+ if (activity.isFinishing()) {
682
+ if (completion != null) {
683
+ completion.run();
684
+ }
685
+ return;
686
+ }
687
+
688
+ if (isCameraPermissionDialogShowing) {
689
+ if (completion != null) {
690
+ completion.run();
691
+ }
692
+ return;
693
+ }
694
+
695
+ AlertDialog dialog = new AlertDialog.Builder(activity)
696
+ .setTitle(title)
697
+ .setMessage(message)
698
+ .setNegativeButton(cancelText, (d, which) -> {
699
+ d.dismiss();
700
+ isCameraPermissionDialogShowing = false;
701
+ })
702
+ .setPositiveButton(openSettingsText, (d, which) -> {
703
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
704
+ Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
705
+ intent.setData(uri);
706
+ activity.startActivity(intent);
707
+ isCameraPermissionDialogShowing = false;
708
+ })
709
+ .setOnDismissListener((d) -> isCameraPermissionDialogShowing = false)
710
+ .create();
711
+
712
+ isCameraPermissionDialogShowing = true;
713
+ dialog.show();
714
+ if (completion != null) {
715
+ completion.run();
716
+ }
717
+ });
718
+ }
719
+
720
+ private String mapPermissionState(PermissionState state) {
721
+ if (state == null) {
722
+ return PermissionState.PROMPT.toString();
723
+ }
724
+
725
+ return state.toString();
726
+ }
727
+
728
+ @PluginMethod
729
+ public void checkPermissions(PluginCall call) {
730
+ boolean disableAudio = call.getBoolean("disableAudio") != null ? Boolean.TRUE.equals(call.getBoolean("disableAudio")) : true;
731
+
732
+ PermissionState cameraState = getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS);
733
+
734
+ JSObject result = new JSObject();
735
+ result.put("camera", mapPermissionState(cameraState));
736
+
737
+ if (!disableAudio) {
738
+ PermissionState audioState = getPermissionState(MICROPHONE_ONLY_PERMISSION_ALIAS);
739
+ result.put("microphone", mapPermissionState(audioState));
740
+ }
741
+
742
+ call.resolve(result);
743
+ }
744
+
745
+ @Override
746
+ @PluginMethod
747
+ public void requestPermissions(PluginCall call) {
748
+ Boolean disableAudioOption = call.getBoolean("disableAudio");
749
+ boolean disableAudio = disableAudioOption == null ? true : Boolean.TRUE.equals(disableAudioOption);
750
+ this.lastDisableAudio = disableAudio;
751
+
752
+ String permissionAlias = disableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
753
+
754
+ boolean cameraGranted = PermissionState.GRANTED.equals(getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS));
755
+ boolean audioGranted = disableAudio || PermissionState.GRANTED.equals(getPermissionState(MICROPHONE_ONLY_PERMISSION_ALIAS));
756
+
757
+ if (cameraGranted && audioGranted) {
758
+ JSObject result = new JSObject();
759
+ result.put("camera", mapPermissionState(PermissionState.GRANTED));
760
+ if (!disableAudio) {
761
+ result.put("microphone", mapPermissionState(PermissionState.GRANTED));
762
+ }
763
+ call.resolve(result);
764
+ return;
765
+ }
766
+
767
+ requestPermissionForAlias(permissionAlias, call, "handleRequestPermissionsResult");
768
+ }
769
+
770
+ @PermissionCallback
771
+ private void handleRequestPermissionsResult(PluginCall call) {
772
+ Boolean disableAudioOption = call.getBoolean("disableAudio");
773
+ boolean disableAudio = disableAudioOption == null ? true : Boolean.TRUE.equals(disableAudioOption);
774
+ this.lastDisableAudio = disableAudio;
775
+
776
+ PermissionState cameraState = getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS);
777
+ JSObject result = new JSObject();
778
+ result.put("camera", mapPermissionState(cameraState));
779
+
780
+ if (!disableAudio) {
781
+ PermissionState audioState = getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS);
782
+ result.put("microphone", mapPermissionState(audioState));
783
+ }
784
+
785
+ boolean showSettingsAlert = call.getBoolean("showSettingsAlert") != null
786
+ ? Boolean.TRUE.equals(call.getBoolean("showSettingsAlert"))
787
+ : false;
788
+
789
+ String cameraStateString = result.getString("camera");
790
+ boolean cameraNeedsSettings = "denied".equals(cameraStateString) || "prompt-with-rationale".equals(cameraStateString);
791
+
792
+ boolean microphoneNeedsSettings = false;
793
+ if (result.has("microphone")) {
794
+ String micStateString = result.getString("microphone");
795
+ microphoneNeedsSettings = "denied".equals(micStateString) || "prompt-with-rationale".equals(micStateString);
796
+ }
797
+
798
+ boolean shouldShowAlert = showSettingsAlert && (cameraNeedsSettings || microphoneNeedsSettings);
1267
799
 
1268
- if (x == -1) {
1269
- // Center horizontally
1270
- int screenWidth = metrics.widthPixels;
1271
- computedX = (screenWidth - computedWidth) / 2;
1272
- Log.d(
1273
- "CameraPreview",
1274
- "Centering horizontally: screenWidth=" +
1275
- screenWidth +
1276
- ", computedWidth=" +
1277
- computedWidth +
1278
- ", computedX=" +
1279
- computedX
1280
- );
800
+ if (shouldShowAlert) {
801
+ Activity activity = getActivity();
802
+ if (activity == null) {
803
+ call.resolve(result);
804
+ return;
805
+ }
806
+
807
+ String title = call.getString("title", "Camera Permission Needed");
808
+ String message = call.getString("message", "Enable camera access in Settings to use the preview.");
809
+ String openSettingsText = call.getString("openSettingsButtonTitle", "Open Settings");
810
+ String cancelText = call.getString("cancelButtonTitle", activity.getString(android.R.string.cancel));
811
+
812
+ showCameraPermissionDialog(title, message, openSettingsText, cancelText, () -> call.resolve(result));
1281
813
  } else {
1282
- computedX = (int) (x * pixelRatio);
1283
- Log.d(
1284
- "CameraPreview",
1285
- "Using provided X position: " +
1286
- x +
1287
- " * " +
1288
- pixelRatio +
1289
- " = " +
1290
- computedX
1291
- );
1292
- }
1293
-
1294
- if (y == -1) {
1295
- // Position vertically based on positioning parameter
1296
- int screenHeight = metrics.heightPixels;
1297
-
1298
- switch (Objects.requireNonNull(positioning)) {
1299
- case "top":
1300
- computedY = 0;
1301
- Log.d("CameraPreview", "Positioning at top: computedY=0");
1302
- break;
1303
- case "bottom":
1304
- computedY = screenHeight - computedHeight;
1305
- Log.d(
1306
- "CameraPreview",
1307
- "Positioning at bottom: screenHeight=" +
1308
- screenHeight +
1309
- ", computedHeight=" +
1310
- computedHeight +
1311
- ", computedY=" +
1312
- computedY
1313
- );
1314
- break;
1315
- case "center":
1316
- default:
1317
- // Center vertically
1318
- if (isEdgeToEdgeActive) {
1319
- // When WebView is offset from top, center within the available space
1320
- // The camera should be centered in the full screen, not just the WebView area
1321
- computedY = (screenHeight - computedHeight) / 2;
814
+ call.resolve(result);
815
+ }
816
+ }
817
+
818
+ @PermissionCallback
819
+ private void handleCameraPermissionResult(PluginCall call) {
820
+ if (
821
+ PermissionState.GRANTED.equals(getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)) ||
822
+ PermissionState.GRANTED.equals(getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS))
823
+ ) {
824
+ startCamera(call);
825
+ } else {
826
+ call.reject("camera permission denied. enable camera access in Settings.", "cameraPermissionDenied");
827
+ }
828
+ }
829
+
830
+ private void startCamera(final PluginCall call) {
831
+ String positionParam = call.getString("position");
832
+ String originalDeviceId = call.getString("deviceId");
833
+ String deviceId = originalDeviceId; // Use a mutable variable
834
+
835
+ final String position = (positionParam == null ||
836
+ positionParam.isEmpty() ||
837
+ "rear".equals(positionParam) ||
838
+ "back".equals(positionParam))
839
+ ? "back"
840
+ : "front";
841
+ // Use -1 as default to indicate centering is needed when x/y not provided
842
+ final Integer xParam = call.getInt("x");
843
+ final Integer yParam = call.getInt("y");
844
+ final int x = xParam != null ? xParam : -1;
845
+ final int y = yParam != null ? yParam : -1;
846
+
847
+ Log.d("CameraPreview", "========================");
848
+ Log.d("CameraPreview", "CAMERA POSITION TRACKING START:");
849
+ Log.d("CameraPreview", "1. RAW PARAMS - xParam: " + xParam + ", yParam: " + yParam);
850
+ Log.d("CameraPreview", "2. AFTER DEFAULT - x: " + x + " (center=" + (x == -1) + "), y: " + y + " (center=" + (y == -1) + ")");
851
+ //noinspection DataFlowIssue
852
+ final int width = call.getInt("width", 0);
853
+ //noinspection DataFlowIssue
854
+ final int height = call.getInt("height", 0);
855
+ //noinspection DataFlowIssue
856
+ final int paddingBottom = call.getInt("paddingBottom", 0);
857
+ final boolean toBack = Boolean.TRUE.equals(call.getBoolean("toBack", true));
858
+ final boolean storeToFile = Boolean.TRUE.equals(call.getBoolean("storeToFile", false));
859
+ final boolean enableOpacity = Boolean.TRUE.equals(call.getBoolean("enableOpacity", false));
860
+ final boolean disableExifHeaderStripping = Boolean.TRUE.equals(call.getBoolean("disableExifHeaderStripping", false));
861
+ final boolean lockOrientation = Boolean.TRUE.equals(call.getBoolean("lockAndroidOrientation", false));
862
+ final boolean disableAudio = Boolean.TRUE.equals(call.getBoolean("disableAudio", true));
863
+ this.lastDisableAudio = disableAudio;
864
+ final String aspectRatio = call.getString("aspectRatio", "4:3");
865
+ final String gridMode = call.getString("gridMode", "none");
866
+ final String positioning = call.getString("positioning", "top");
867
+ //noinspection DataFlowIssue
868
+ final float initialZoomLevel = call.getFloat("initialZoomLevel", 1.0f);
869
+ //noinspection DataFlowIssue
870
+ final boolean disableFocusIndicator = call.getBoolean("disableFocusIndicator", false);
871
+ final boolean enableVideoMode = Boolean.TRUE.equals(call.getBoolean("enableVideoMode", false));
872
+
873
+ // Check for conflict between aspectRatio and size
874
+ if (call.getData().has("aspectRatio") && (call.getData().has("width") || call.getData().has("height"))) {
875
+ call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.");
876
+ return;
877
+ }
878
+
879
+ float targetZoom = initialZoomLevel;
880
+ // Check if the selected device is a physical ultra-wide
881
+ if (originalDeviceId != null) {
882
+ List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(getContext());
883
+ for (CameraDevice device : devices) {
884
+ if (originalDeviceId.equals(device.getDeviceId()) && !device.isLogical()) {
885
+ for (LensInfo lens : device.getLenses()) {
886
+ if ("ultraWide".equals(lens.getDeviceType())) {
887
+ Log.d("CameraPreview", "Ultra-wide lens selected. Targeting 0.5x zoom on logical camera.");
888
+ targetZoom = 0.5f;
889
+ // Force the use of the logical camera by clearing the specific deviceId
890
+ deviceId = null;
891
+ break;
892
+ }
893
+ }
894
+ }
895
+ if (deviceId == null) break; // Exit outer loop once we've made our decision
896
+ }
897
+ }
898
+
899
+ previousOrientationRequest = getBridge().getActivity().getRequestedOrientation();
900
+ cameraXView = new CameraXView(getContext(), getBridge().getWebView());
901
+ cameraXView.setListener(this);
902
+
903
+ String finalDeviceId = deviceId;
904
+ float finalTargetZoom = targetZoom;
905
+ getBridge()
906
+ .getActivity()
907
+ .runOnUiThread(() -> {
908
+ // Ensure transparent background when preview is behind the WebView (Android 10 fix)
909
+ if (toBack) {
910
+ try {
911
+ if (originalWindowBackground == null) {
912
+ originalWindowBackground = getBridge().getActivity().getWindow().getDecorView().getBackground();
913
+ }
914
+ getBridge().getActivity().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
915
+ } catch (Exception ignored) {}
916
+ }
917
+ DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
918
+ if (lockOrientation) {
919
+ getBridge().getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
920
+ }
921
+
922
+ // Debug: Let's check all the positioning information
923
+ ViewGroup webViewParent = (ViewGroup) getBridge().getWebView().getParent();
924
+
925
+ // Get webview position in different coordinate systems
926
+ int[] webViewLocationInWindow = new int[2];
927
+ int[] webViewLocationOnScreen = new int[2];
928
+ getBridge().getWebView().getLocationInWindow(webViewLocationInWindow);
929
+ getBridge().getWebView().getLocationOnScreen(webViewLocationOnScreen);
930
+
931
+ int webViewLeft = getBridge().getWebView().getLeft();
932
+ int webViewTop = getBridge().getWebView().getTop();
933
+
934
+ // Check parent position too
935
+ int[] parentLocationInWindow = new int[2];
936
+ int[] parentLocationOnScreen = new int[2];
937
+ webViewParent.getLocationInWindow(parentLocationInWindow);
938
+ webViewParent.getLocationOnScreen(parentLocationOnScreen);
939
+
940
+ // Calculate pixel ratio
941
+ float pixelRatio = metrics.density;
942
+
943
+ // The key insight: JavaScript coordinates are relative to the WebView's viewport
944
+ // If the WebView is positioned below the status bar (webViewLocationOnScreen[1] > 0),
945
+ // we need to add that offset when placing native views
946
+ int webViewTopInset = webViewLocationOnScreen[1];
947
+ boolean isEdgeToEdgeActive = webViewLocationOnScreen[1] > 0;
948
+
949
+ // Log all the positioning information for debugging
950
+ Log.d("CameraPreview", "WebView Position Debug:");
951
+ Log.d("CameraPreview", " - webView.getTop(): " + webViewTop);
952
+ Log.d("CameraPreview", " - webView.getLeft(): " + webViewLeft);
1322
953
  Log.d(
1323
- "CameraPreview",
1324
- "Centering vertically with WebView offset: screenHeight=" +
1325
- screenHeight +
1326
- ", webViewTop=" +
1327
- webViewTopInset +
1328
- ", computedHeight=" +
1329
- computedHeight +
1330
- ", computedY=" +
1331
- computedY
954
+ "CameraPreview",
955
+ " - webView locationInWindow: (" + webViewLocationInWindow[0] + ", " + webViewLocationInWindow[1] + ")"
1332
956
  );
1333
- } else {
1334
- // Normal mode - use full screen height
1335
- computedY = (screenHeight - computedHeight) / 2;
1336
957
  Log.d(
1337
- "CameraPreview",
1338
- "Centering vertically (normal): screenHeight=" +
1339
- screenHeight +
1340
- ", computedHeight=" +
1341
- computedHeight +
1342
- ", computedY=" +
1343
- computedY
958
+ "CameraPreview",
959
+ " - webView locationOnScreen: (" + webViewLocationOnScreen[0] + ", " + webViewLocationOnScreen[1] + ")"
960
+ );
961
+ Log.d(
962
+ "CameraPreview",
963
+ " - parent locationInWindow: (" + parentLocationInWindow[0] + ", " + parentLocationInWindow[1] + ")"
964
+ );
965
+ Log.d(
966
+ "CameraPreview",
967
+ " - parent locationOnScreen: (" + parentLocationOnScreen[0] + ", " + parentLocationOnScreen[1] + ")"
1344
968
  );
1345
- }
1346
- break;
1347
- }
1348
- } else {
1349
- computedY = (int) (y * pixelRatio);
1350
- // If edge-to-edge is active, JavaScript Y is relative to WebView content area
1351
- // We need to add the inset to get absolute screen position
1352
- if (isEdgeToEdgeActive) {
1353
- computedY += webViewTopInset;
1354
- Log.d(
1355
- "CameraPreview",
1356
- "Edge-to-edge adjustment: Y position " +
1357
- (int) (y * pixelRatio) +
1358
- " + inset " +
1359
- webViewTopInset +
1360
- " = " +
1361
- computedY
1362
- );
1363
- }
1364
- Log.d(
1365
- "CameraPreview",
1366
- "Using provided Y position: " +
1367
- y +
1368
- " * " +
1369
- pixelRatio +
1370
- " = " +
1371
- computedY +
1372
- (isEdgeToEdgeActive ? " (adjusted for edge-to-edge)" : "")
1373
- );
1374
- }
1375
969
 
1376
- Log.d(
1377
- "CameraPreview",
1378
- "2b. EDGE-TO-EDGE - " +
1379
- (isEdgeToEdgeActive
1380
- ? "ACTIVE (inset=" + webViewTopInset + ")"
1381
- : "INACTIVE")
1382
- );
1383
- Log.d(
1384
- "CameraPreview",
1385
- "3. COMPUTED POSITION - x=" + computedX + ", y=" + computedY
1386
- );
1387
- Log.d(
1388
- "CameraPreview",
1389
- "4. COMPUTED SIZE - width=" +
1390
- computedWidth +
1391
- ", height=" +
1392
- computedHeight
1393
- );
1394
- Log.d("CameraPreview", "=== COORDINATE DEBUG ===");
1395
- Log.d(
1396
- "CameraPreview",
1397
- "WebView getLeft/getTop: (" + webViewLeft + ", " + webViewTop + ")"
1398
- );
1399
- Log.d(
1400
- "CameraPreview",
1401
- "WebView locationInWindow: (" +
1402
- webViewLocationInWindow[0] +
1403
- ", " +
1404
- webViewLocationInWindow[1] +
1405
- ")"
1406
- );
1407
- Log.d(
1408
- "CameraPreview",
1409
- "WebView locationOnScreen: (" +
1410
- webViewLocationOnScreen[0] +
1411
- ", " +
1412
- webViewLocationOnScreen[1] +
1413
- ")"
1414
- );
1415
- Log.d(
1416
- "CameraPreview",
1417
- "Parent locationInWindow: (" +
1418
- parentLocationInWindow[0] +
1419
- ", " +
1420
- parentLocationInWindow[1] +
1421
- ")"
1422
- );
1423
- Log.d(
1424
- "CameraPreview",
1425
- "Parent locationOnScreen: (" +
1426
- parentLocationOnScreen[0] +
1427
- ", " +
1428
- parentLocationOnScreen[1] +
1429
- ")"
1430
- );
1431
- Log.d(
1432
- "CameraPreview",
1433
- "Parent class: " + webViewParent.getClass().getSimpleName()
1434
- );
1435
- Log.d(
1436
- "CameraPreview",
1437
- "Requested position (logical): (" + x + ", " + y + ")"
1438
- );
1439
- Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
1440
- Log.d(
1441
- "CameraPreview",
1442
- "Final computed position (no offset): (" +
1443
- computedX +
1444
- ", " +
1445
- computedY +
1446
- ")"
1447
- );
1448
- Log.d("CameraPreview", "5. IS_CENTERED - " + (x == -1 || y == -1));
1449
- Log.d("CameraPreview", "========================");
970
+ // Check if WebView has margins
971
+ View webView = getBridge().getWebView();
972
+ ViewGroup.LayoutParams webViewLayoutParams = webView.getLayoutParams();
973
+ if (webViewLayoutParams instanceof ViewGroup.MarginLayoutParams) {
974
+ ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) webViewLayoutParams;
975
+ Log.d(
976
+ "CameraPreview",
977
+ " - webView margins: left=" +
978
+ marginParams.leftMargin +
979
+ ", top=" +
980
+ marginParams.topMargin +
981
+ ", right=" +
982
+ marginParams.rightMargin +
983
+ ", bottom=" +
984
+ marginParams.bottomMargin
985
+ );
986
+ }
1450
987
 
1451
- // Pass along whether we're centering so CameraXView knows not to add insets
1452
- boolean isCentered = (x == -1 || y == -1);
1453
-
1454
- CameraSessionConfiguration config = new CameraSessionConfiguration(
1455
- finalDeviceId,
1456
- position,
1457
- computedX,
1458
- computedY,
1459
- computedWidth,
1460
- computedHeight,
1461
- paddingBottom,
1462
- toBack,
1463
- storeToFile,
1464
- enableOpacity,
1465
- disableExifHeaderStripping,
1466
- disableAudio,
1467
- 1.0f,
1468
- aspectRatio,
1469
- gridMode,
1470
- disableFocusIndicator,
1471
- enableVideoMode
1472
- );
1473
- config.setTargetZoom(finalTargetZoom);
1474
- config.setCentered(isCentered);
988
+ // Check WebView padding
989
+ Log.d(
990
+ "CameraPreview",
991
+ " - webView padding: left=" +
992
+ webView.getPaddingLeft() +
993
+ ", top=" +
994
+ webView.getPaddingTop() +
995
+ ", right=" +
996
+ webView.getPaddingRight() +
997
+ ", bottom=" +
998
+ webView.getPaddingBottom()
999
+ );
1475
1000
 
1476
- bridge.saveCall(call);
1477
- cameraStartCallbackId = call.getCallbackId();
1478
- cameraXView.startSession(config);
1479
-
1480
- // Setup orientation listener to mirror iOS screenResize emission
1481
- if (orientationListener == null) {
1482
- lastOrientation = getContext()
1483
- .getResources()
1484
- .getConfiguration()
1485
- .orientation;
1486
- lastOrientationStr = getDeviceOrientationString();
1487
- orientationListener = new OrientationEventListener(getContext()) {
1488
- @Override
1489
- public void onOrientationChanged(int orientation) {
1490
- if (orientation == ORIENTATION_UNKNOWN) return;
1491
- int current = getContext()
1492
- .getResources()
1493
- .getConfiguration()
1494
- .orientation;
1495
- String currentStr = getDeviceOrientationString();
1496
- if (
1497
- current != lastOrientation ||
1498
- !Objects.equals(currentStr, lastOrientationStr)
1499
- ) {
1500
- lastOrientation = current;
1501
- lastOrientationStr = currentStr;
1502
- // Post to next frame so WebView has updated bounds before we recompute layout
1503
- getBridge()
1504
- .getActivity()
1505
- .getWindow()
1506
- .getDecorView()
1507
- .post(() -> handleOrientationChange());
1508
- }
1509
- }
1510
- };
1511
- if (orientationListener.canDetectOrientation()) {
1512
- orientationListener.enable();
1513
- }
1514
- }
1515
- });
1516
- }
1517
-
1518
- private void handleOrientationChange() {
1519
- if (cameraXView == null || !cameraXView.isRunning()) return;
1520
-
1521
- Log.d(
1522
- TAG,
1523
- "======================== ORIENTATION CHANGE DETECTED ========================"
1524
- );
1525
-
1526
- // Get comprehensive display and orientation information
1527
- android.util.DisplayMetrics metrics = getContext()
1528
- .getResources()
1529
- .getDisplayMetrics();
1530
- int screenWidthPx = metrics.widthPixels;
1531
- int screenHeightPx = metrics.heightPixels;
1532
- float density = metrics.density;
1533
- int screenWidthDp = (int) (screenWidthPx / density);
1534
- int screenHeightDp = (int) (screenHeightPx / density);
1535
-
1536
- int current = getContext().getResources().getConfiguration().orientation;
1537
- Log.d(TAG, "New orientation: " + current + " (1=PORTRAIT, 2=LANDSCAPE)");
1538
- Log.d(
1539
- TAG,
1540
- "Screen dimensions - Pixels: " +
1541
- screenWidthPx +
1542
- "x" +
1543
- screenHeightPx +
1544
- ", DP: " +
1545
- screenWidthDp +
1546
- "x" +
1547
- screenHeightDp +
1548
- ", Density: " +
1549
- density
1550
- );
1551
-
1552
- // Get WebView dimensions before rotation
1553
- WebView webView = getBridge().getWebView();
1554
- int webViewWidth = webView.getWidth();
1555
- int webViewHeight = webView.getHeight();
1556
- Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
1557
-
1558
- // Get current preview bounds before rotation
1559
- int[] oldBounds = cameraXView.getCurrentPreviewBounds();
1560
- Log.d(
1561
- TAG,
1562
- "Current preview bounds before rotation: x=" +
1563
- oldBounds[0] +
1564
- ", y=" +
1565
- oldBounds[1] +
1566
- ", width=" +
1567
- oldBounds[2] +
1568
- ", height=" +
1569
- oldBounds[3]
1570
- );
1571
-
1572
- getBridge()
1573
- .getActivity()
1574
- .runOnUiThread(() -> {
1575
- // Create and show a black full-screen overlay during rotation
1576
- ViewGroup rootView = (ViewGroup) getBridge()
1577
- .getActivity()
1578
- .getWindow()
1579
- .getDecorView()
1580
- .getRootView();
1581
-
1582
- // Remove any existing overlay
1583
- if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1584
- ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
1585
- }
1586
-
1587
- // Create new black overlay
1588
- rotationOverlay = new View(getContext());
1589
- rotationOverlay.setBackgroundColor(Color.BLACK);
1590
- ViewGroup.LayoutParams overlayParams = new ViewGroup.LayoutParams(
1591
- ViewGroup.LayoutParams.MATCH_PARENT,
1592
- ViewGroup.LayoutParams.MATCH_PARENT
1593
- );
1594
- rotationOverlay.setLayoutParams(overlayParams);
1595
- rootView.addView(rotationOverlay);
1596
-
1597
- // Reapply current aspect ratio to recompute layout, then emit screenResize
1598
- String ar = cameraXView.getAspectRatio();
1599
- Log.d(TAG, "Reapplying aspect ratio: " + ar);
1600
-
1601
- // Re-get dimensions after potential layout pass
1602
- android.util.DisplayMetrics newMetrics = getContext()
1603
- .getResources()
1604
- .getDisplayMetrics();
1605
- int newScreenWidthPx = newMetrics.widthPixels;
1606
- int newScreenHeightPx = newMetrics.heightPixels;
1607
- int newWebViewWidth = webView.getWidth();
1608
- int newWebViewHeight = webView.getHeight();
1001
+ Log.d("CameraPreview", " - Using webViewTopInset: " + webViewTopInset);
1002
+ Log.d("CameraPreview", " - isEdgeToEdgeActive: " + isEdgeToEdgeActive);
1003
+
1004
+ // Calculate position - center if x or y is -1
1005
+ int computedX;
1006
+ int computedY;
1609
1007
 
1008
+ // Calculate dimensions first
1009
+ int computedWidth = width != 0 ? (int) (width * pixelRatio) : getBridge().getWebView().getWidth();
1010
+ int computedHeight = height != 0 ? (int) (height * pixelRatio) : getBridge().getWebView().getHeight();
1011
+ computedHeight -= (int) (paddingBottom * pixelRatio);
1012
+
1013
+ Log.d("CameraPreview", "========================");
1014
+ Log.d("CameraPreview", "POSITIONING CALCULATIONS:");
1015
+ Log.d("CameraPreview", "1. INPUT - x: " + x + ", y: " + y + ", width: " + width + ", height: " + height);
1016
+ Log.d("CameraPreview", "2. PIXEL RATIO: " + pixelRatio);
1017
+ Log.d("CameraPreview", "3. SCREEN - width: " + metrics.widthPixels + ", height: " + metrics.heightPixels);
1018
+ Log.d(
1019
+ "CameraPreview",
1020
+ "4. WEBVIEW - width: " + getBridge().getWebView().getWidth() + ", height: " + getBridge().getWebView().getHeight()
1021
+ );
1022
+ Log.d("CameraPreview", "5. COMPUTED DIMENSIONS - width: " + computedWidth + ", height: " + computedHeight);
1023
+
1024
+ if (x == -1) {
1025
+ // Center horizontally
1026
+ int screenWidth = metrics.widthPixels;
1027
+ computedX = (screenWidth - computedWidth) / 2;
1028
+ Log.d(
1029
+ "CameraPreview",
1030
+ "Centering horizontally: screenWidth=" +
1031
+ screenWidth +
1032
+ ", computedWidth=" +
1033
+ computedWidth +
1034
+ ", computedX=" +
1035
+ computedX
1036
+ );
1037
+ } else {
1038
+ computedX = (int) (x * pixelRatio);
1039
+ Log.d("CameraPreview", "Using provided X position: " + x + " * " + pixelRatio + " = " + computedX);
1040
+ }
1041
+
1042
+ if (y == -1) {
1043
+ // Position vertically based on positioning parameter
1044
+ int screenHeight = metrics.heightPixels;
1045
+
1046
+ switch (Objects.requireNonNull(positioning)) {
1047
+ case "top":
1048
+ computedY = 0;
1049
+ Log.d("CameraPreview", "Positioning at top: computedY=0");
1050
+ break;
1051
+ case "bottom":
1052
+ computedY = screenHeight - computedHeight;
1053
+ Log.d(
1054
+ "CameraPreview",
1055
+ "Positioning at bottom: screenHeight=" +
1056
+ screenHeight +
1057
+ ", computedHeight=" +
1058
+ computedHeight +
1059
+ ", computedY=" +
1060
+ computedY
1061
+ );
1062
+ break;
1063
+ case "center":
1064
+ default:
1065
+ // Center vertically
1066
+ if (isEdgeToEdgeActive) {
1067
+ // When WebView is offset from top, center within the available space
1068
+ // The camera should be centered in the full screen, not just the WebView area
1069
+ computedY = (screenHeight - computedHeight) / 2;
1070
+ Log.d(
1071
+ "CameraPreview",
1072
+ "Centering vertically with WebView offset: screenHeight=" +
1073
+ screenHeight +
1074
+ ", webViewTop=" +
1075
+ webViewTopInset +
1076
+ ", computedHeight=" +
1077
+ computedHeight +
1078
+ ", computedY=" +
1079
+ computedY
1080
+ );
1081
+ } else {
1082
+ // Normal mode - use full screen height
1083
+ computedY = (screenHeight - computedHeight) / 2;
1084
+ Log.d(
1085
+ "CameraPreview",
1086
+ "Centering vertically (normal): screenHeight=" +
1087
+ screenHeight +
1088
+ ", computedHeight=" +
1089
+ computedHeight +
1090
+ ", computedY=" +
1091
+ computedY
1092
+ );
1093
+ }
1094
+ break;
1095
+ }
1096
+ } else {
1097
+ computedY = (int) (y * pixelRatio);
1098
+ // If edge-to-edge is active, JavaScript Y is relative to WebView content area
1099
+ // We need to add the inset to get absolute screen position
1100
+ if (isEdgeToEdgeActive) {
1101
+ computedY += webViewTopInset;
1102
+ Log.d(
1103
+ "CameraPreview",
1104
+ "Edge-to-edge adjustment: Y position " +
1105
+ (int) (y * pixelRatio) +
1106
+ " + inset " +
1107
+ webViewTopInset +
1108
+ " = " +
1109
+ computedY
1110
+ );
1111
+ }
1112
+ Log.d(
1113
+ "CameraPreview",
1114
+ "Using provided Y position: " +
1115
+ y +
1116
+ " * " +
1117
+ pixelRatio +
1118
+ " = " +
1119
+ computedY +
1120
+ (isEdgeToEdgeActive ? " (adjusted for edge-to-edge)" : "")
1121
+ );
1122
+ }
1123
+
1124
+ Log.d(
1125
+ "CameraPreview",
1126
+ "2b. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE (inset=" + webViewTopInset + ")" : "INACTIVE")
1127
+ );
1128
+ Log.d("CameraPreview", "3. COMPUTED POSITION - x=" + computedX + ", y=" + computedY);
1129
+ Log.d("CameraPreview", "4. COMPUTED SIZE - width=" + computedWidth + ", height=" + computedHeight);
1130
+ Log.d("CameraPreview", "=== COORDINATE DEBUG ===");
1131
+ Log.d("CameraPreview", "WebView getLeft/getTop: (" + webViewLeft + ", " + webViewTop + ")");
1132
+ Log.d(
1133
+ "CameraPreview",
1134
+ "WebView locationInWindow: (" + webViewLocationInWindow[0] + ", " + webViewLocationInWindow[1] + ")"
1135
+ );
1136
+ Log.d(
1137
+ "CameraPreview",
1138
+ "WebView locationOnScreen: (" + webViewLocationOnScreen[0] + ", " + webViewLocationOnScreen[1] + ")"
1139
+ );
1140
+ Log.d("CameraPreview", "Parent locationInWindow: (" + parentLocationInWindow[0] + ", " + parentLocationInWindow[1] + ")");
1141
+ Log.d("CameraPreview", "Parent locationOnScreen: (" + parentLocationOnScreen[0] + ", " + parentLocationOnScreen[1] + ")");
1142
+ Log.d("CameraPreview", "Parent class: " + webViewParent.getClass().getSimpleName());
1143
+ Log.d("CameraPreview", "Requested position (logical): (" + x + ", " + y + ")");
1144
+ Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
1145
+ Log.d("CameraPreview", "Final computed position (no offset): (" + computedX + ", " + computedY + ")");
1146
+ Log.d("CameraPreview", "5. IS_CENTERED - " + (x == -1 || y == -1));
1147
+ Log.d("CameraPreview", "========================");
1148
+
1149
+ // Pass along whether we're centering so CameraXView knows not to add insets
1150
+ boolean isCentered = (x == -1 || y == -1);
1151
+
1152
+ CameraSessionConfiguration config = new CameraSessionConfiguration(
1153
+ finalDeviceId,
1154
+ position,
1155
+ computedX,
1156
+ computedY,
1157
+ computedWidth,
1158
+ computedHeight,
1159
+ paddingBottom,
1160
+ toBack,
1161
+ storeToFile,
1162
+ enableOpacity,
1163
+ disableExifHeaderStripping,
1164
+ disableAudio,
1165
+ 1.0f,
1166
+ aspectRatio,
1167
+ gridMode,
1168
+ disableFocusIndicator,
1169
+ enableVideoMode
1170
+ );
1171
+ config.setTargetZoom(finalTargetZoom);
1172
+ config.setCentered(isCentered);
1173
+
1174
+ bridge.saveCall(call);
1175
+ cameraStartCallbackId = call.getCallbackId();
1176
+ cameraXView.startSession(config);
1177
+
1178
+ // Setup orientation listener to mirror iOS screenResize emission
1179
+ if (orientationListener == null) {
1180
+ lastOrientation = getContext().getResources().getConfiguration().orientation;
1181
+ lastOrientationStr = getDeviceOrientationString();
1182
+ orientationListener = new OrientationEventListener(getContext()) {
1183
+ @Override
1184
+ public void onOrientationChanged(int orientation) {
1185
+ if (orientation == ORIENTATION_UNKNOWN) return;
1186
+ int current = getContext().getResources().getConfiguration().orientation;
1187
+ String currentStr = getDeviceOrientationString();
1188
+ if (current != lastOrientation || !Objects.equals(currentStr, lastOrientationStr)) {
1189
+ lastOrientation = current;
1190
+ lastOrientationStr = currentStr;
1191
+ // Post to next frame so WebView has updated bounds before we recompute layout
1192
+ getBridge()
1193
+ .getActivity()
1194
+ .getWindow()
1195
+ .getDecorView()
1196
+ .post(() -> handleOrientationChange());
1197
+ }
1198
+ }
1199
+ };
1200
+ if (orientationListener.canDetectOrientation()) {
1201
+ orientationListener.enable();
1202
+ }
1203
+ }
1204
+ });
1205
+ }
1206
+
1207
+ private void handleOrientationChange() {
1208
+ if (cameraXView == null || !cameraXView.isRunning()) return;
1209
+
1210
+ Log.d(TAG, "======================== ORIENTATION CHANGE DETECTED ========================");
1211
+
1212
+ // Get comprehensive display and orientation information
1213
+ android.util.DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
1214
+ int screenWidthPx = metrics.widthPixels;
1215
+ int screenHeightPx = metrics.heightPixels;
1216
+ float density = metrics.density;
1217
+ int screenWidthDp = (int) (screenWidthPx / density);
1218
+ int screenHeightDp = (int) (screenHeightPx / density);
1219
+
1220
+ int current = getContext().getResources().getConfiguration().orientation;
1221
+ Log.d(TAG, "New orientation: " + current + " (1=PORTRAIT, 2=LANDSCAPE)");
1610
1222
  Log.d(
1611
- TAG,
1612
- "New screen dimensions after rotation: " +
1613
- newScreenWidthPx +
1614
- "x" +
1615
- newScreenHeightPx
1223
+ TAG,
1224
+ "Screen dimensions - Pixels: " +
1225
+ screenWidthPx +
1226
+ "x" +
1227
+ screenHeightPx +
1228
+ ", DP: " +
1229
+ screenWidthDp +
1230
+ "x" +
1231
+ screenHeightDp +
1232
+ ", Density: " +
1233
+ density
1616
1234
  );
1235
+
1236
+ // Get WebView dimensions before rotation
1237
+ WebView webView = getBridge().getWebView();
1238
+ int webViewWidth = webView.getWidth();
1239
+ int webViewHeight = webView.getHeight();
1240
+ Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
1241
+
1242
+ // Get current preview bounds before rotation
1243
+ int[] oldBounds = cameraXView.getCurrentPreviewBounds();
1617
1244
  Log.d(
1618
- TAG,
1619
- "New WebView dimensions after rotation: " +
1620
- newWebViewWidth +
1621
- "x" +
1622
- newWebViewHeight
1245
+ TAG,
1246
+ "Current preview bounds before rotation: x=" +
1247
+ oldBounds[0] +
1248
+ ", y=" +
1249
+ oldBounds[1] +
1250
+ ", width=" +
1251
+ oldBounds[2] +
1252
+ ", height=" +
1253
+ oldBounds[3]
1623
1254
  );
1624
1255
 
1625
- // Force aspect ratio recalculation on orientation change
1626
- cameraXView.forceAspectRatioRecalculation(ar, null, null, () -> {
1627
- int[] bounds = cameraXView.getCurrentPreviewBounds();
1628
- Log.d(
1629
- TAG,
1630
- "New bounds after orientation change: x=" +
1631
- bounds[0] +
1632
- ", y=" +
1633
- bounds[1] +
1634
- ", width=" +
1635
- bounds[2] +
1636
- ", height=" +
1637
- bounds[3]
1638
- );
1639
- Log.d(
1640
- TAG,
1641
- "Bounds change: deltaX=" +
1642
- (bounds[0] - oldBounds[0]) +
1643
- ", deltaY=" +
1644
- (bounds[1] - oldBounds[1]) +
1645
- ", deltaWidth=" +
1646
- (bounds[2] - oldBounds[2]) +
1647
- ", deltaHeight=" +
1648
- (bounds[3] - oldBounds[3])
1649
- );
1650
-
1651
- JSObject data = new JSObject();
1652
- data.put("x", bounds[0]);
1653
- data.put("y", bounds[1]);
1654
- data.put("width", bounds[2]);
1655
- data.put("height", bounds[3]);
1656
- notifyListeners("screenResize", data);
1657
-
1658
- // Also emit orientationChange with a unified string value matching iOS
1659
- String o = getDeviceOrientationString();
1660
- JSObject oData = new JSObject();
1661
- oData.put("orientation", o);
1662
- notifyListeners("orientationChange", oData);
1663
-
1664
- // Don't remove the overlay here - wait for camera to fully start
1665
- // The overlay will be removed after a delay to ensure camera is stable
1666
- if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1667
- // Shorter delay for faster transition
1668
- int delay = "4:3".equals(ar) ? 200 : 150;
1669
- rotationOverlay.postDelayed(
1670
- () -> {
1671
- if (
1672
- rotationOverlay != null && rotationOverlay.getParent() != null
1673
- ) {
1674
- rotationOverlay
1675
- .animate()
1676
- .alpha(0f)
1677
- .setDuration(100) // Faster fade out
1678
- .withEndAction(() -> {
1679
- if (
1680
- rotationOverlay != null &&
1681
- rotationOverlay.getParent() != null
1682
- ) {
1683
- ((ViewGroup) rotationOverlay.getParent()).removeView(
1684
- rotationOverlay
1685
- );
1686
- rotationOverlay = null;
1687
- }
1688
- })
1689
- .start();
1256
+ getBridge()
1257
+ .getActivity()
1258
+ .runOnUiThread(() -> {
1259
+ // Create and show a black full-screen overlay during rotation
1260
+ ViewGroup rootView = (ViewGroup) getBridge().getActivity().getWindow().getDecorView().getRootView();
1261
+
1262
+ // Remove any existing overlay
1263
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1264
+ ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
1265
+ }
1266
+
1267
+ // Create new black overlay
1268
+ rotationOverlay = new View(getContext());
1269
+ rotationOverlay.setBackgroundColor(Color.BLACK);
1270
+ ViewGroup.LayoutParams overlayParams = new ViewGroup.LayoutParams(
1271
+ ViewGroup.LayoutParams.MATCH_PARENT,
1272
+ ViewGroup.LayoutParams.MATCH_PARENT
1273
+ );
1274
+ rotationOverlay.setLayoutParams(overlayParams);
1275
+ rootView.addView(rotationOverlay);
1276
+
1277
+ // Reapply current aspect ratio to recompute layout, then emit screenResize
1278
+ String ar = cameraXView.getAspectRatio();
1279
+ Log.d(TAG, "Reapplying aspect ratio: " + ar);
1280
+
1281
+ // Re-get dimensions after potential layout pass
1282
+ android.util.DisplayMetrics newMetrics = getContext().getResources().getDisplayMetrics();
1283
+ int newScreenWidthPx = newMetrics.widthPixels;
1284
+ int newScreenHeightPx = newMetrics.heightPixels;
1285
+ int newWebViewWidth = webView.getWidth();
1286
+ int newWebViewHeight = webView.getHeight();
1287
+
1288
+ Log.d(TAG, "New screen dimensions after rotation: " + newScreenWidthPx + "x" + newScreenHeightPx);
1289
+ Log.d(TAG, "New WebView dimensions after rotation: " + newWebViewWidth + "x" + newWebViewHeight);
1290
+
1291
+ // Force aspect ratio recalculation on orientation change
1292
+ cameraXView.forceAspectRatioRecalculation(ar, null, null, () -> {
1293
+ int[] bounds = cameraXView.getCurrentPreviewBounds();
1294
+ Log.d(
1295
+ TAG,
1296
+ "New bounds after orientation change: x=" +
1297
+ bounds[0] +
1298
+ ", y=" +
1299
+ bounds[1] +
1300
+ ", width=" +
1301
+ bounds[2] +
1302
+ ", height=" +
1303
+ bounds[3]
1304
+ );
1305
+ Log.d(
1306
+ TAG,
1307
+ "Bounds change: deltaX=" +
1308
+ (bounds[0] - oldBounds[0]) +
1309
+ ", deltaY=" +
1310
+ (bounds[1] - oldBounds[1]) +
1311
+ ", deltaWidth=" +
1312
+ (bounds[2] - oldBounds[2]) +
1313
+ ", deltaHeight=" +
1314
+ (bounds[3] - oldBounds[3])
1315
+ );
1316
+
1317
+ JSObject data = new JSObject();
1318
+ data.put("x", bounds[0]);
1319
+ data.put("y", bounds[1]);
1320
+ data.put("width", bounds[2]);
1321
+ data.put("height", bounds[3]);
1322
+ notifyListeners("screenResize", data);
1323
+
1324
+ // Also emit orientationChange with a unified string value matching iOS
1325
+ String o = getDeviceOrientationString();
1326
+ JSObject oData = new JSObject();
1327
+ oData.put("orientation", o);
1328
+ notifyListeners("orientationChange", oData);
1329
+
1330
+ // Don't remove the overlay here - wait for camera to fully start
1331
+ // The overlay will be removed after a delay to ensure camera is stable
1332
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1333
+ // Shorter delay for faster transition
1334
+ int delay = "4:3".equals(ar) ? 200 : 150;
1335
+ rotationOverlay.postDelayed(
1336
+ () -> {
1337
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1338
+ rotationOverlay
1339
+ .animate()
1340
+ .alpha(0f)
1341
+ .setDuration(100) // Faster fade out
1342
+ .withEndAction(() -> {
1343
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1344
+ ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
1345
+ rotationOverlay = null;
1346
+ }
1347
+ })
1348
+ .start();
1349
+ }
1350
+ },
1351
+ delay
1352
+ );
1353
+ }
1354
+
1355
+ Log.d(TAG, "================================================================================");
1356
+ });
1357
+ });
1358
+ }
1359
+
1360
+ /**
1361
+ * Compute a canonical orientation string matching iOS values:
1362
+ * "portrait", "portrait-upside-down", "landscape-left", "landscape-right", or "unknown".
1363
+ * Uses display rotation when available, with a fallback to configuration orientation.
1364
+ */
1365
+ private String getDeviceOrientationString() {
1366
+ try {
1367
+ int rotation = -1;
1368
+ // Try to obtain display rotation in a backward/forward-compatible way
1369
+ if (android.os.Build.VERSION.SDK_INT >= 30) {
1370
+ android.view.Display display = getBridge().getActivity().getDisplay();
1371
+ if (display != null) {
1372
+ rotation = display.getRotation();
1690
1373
  }
1691
- },
1692
- delay
1374
+ } else {
1375
+ android.view.Display display = getBridge().getActivity().getWindowManager().getDefaultDisplay();
1376
+ if (display != null) {
1377
+ rotation = display.getRotation();
1378
+ }
1379
+ }
1380
+
1381
+ if (rotation == android.view.Surface.ROTATION_0) {
1382
+ return "portrait";
1383
+ } else if (rotation == android.view.Surface.ROTATION_90) {
1384
+ return "landscape-right";
1385
+ } else if (rotation == android.view.Surface.ROTATION_180) {
1386
+ return "portrait-upside-down";
1387
+ } else if (rotation == android.view.Surface.ROTATION_270) {
1388
+ return "landscape-left";
1389
+ }
1390
+
1391
+ // Fallback to configuration if rotation unavailable
1392
+ int orientation = getContext().getResources().getConfiguration().orientation;
1393
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) return "portrait";
1394
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) return "landscape-right"; // default, avoid generic
1395
+ return "unknown";
1396
+ } catch (Throwable t) {
1397
+ Log.w(TAG, "Failed to get precise orientation, falling back: " + t);
1398
+ int orientation = getContext().getResources().getConfiguration().orientation;
1399
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) return "portrait";
1400
+ if (orientation == Configuration.ORIENTATION_LANDSCAPE) return "landscape-right"; // default, avoid generic
1401
+ return "unknown";
1402
+ }
1403
+ }
1404
+
1405
+ @Override
1406
+ public void onPictureTaken(String base64, JSONObject exif) {
1407
+ PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
1408
+ if (pluginCall == null) {
1409
+ Log.e("CameraPreview", "onPictureTaken: captureCallbackId is null");
1410
+ return;
1411
+ }
1412
+ JSObject result = new JSObject();
1413
+ result.put("value", base64);
1414
+ result.put("exif", exif);
1415
+ pluginCall.resolve(result);
1416
+ bridge.releaseCall(pluginCall);
1417
+ }
1418
+
1419
+ @Override
1420
+ public void onPictureTakenError(String message) {
1421
+ PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
1422
+ if (pluginCall == null) {
1423
+ Log.e("CameraPreview", "onPictureTakenError: captureCallbackId is null");
1424
+ return;
1425
+ }
1426
+ pluginCall.reject(message);
1427
+ bridge.releaseCall(pluginCall);
1428
+ }
1429
+
1430
+ @Override
1431
+ public void onCameraStopped(CameraXView source) {
1432
+ if (cameraXView != null && cameraXView != source) {
1433
+ Log.d(TAG, "onCameraStopped: ignoring callback from stale instance");
1434
+ return;
1435
+ }
1436
+ // Ensure reference is cleared once the originating CameraXView has fully stopped
1437
+ if (cameraXView == source) {
1438
+ cameraXView = null;
1439
+ }
1440
+
1441
+ PluginCall queuedCall = null;
1442
+ synchronized (pendingStartLock) {
1443
+ if (pendingStartCall != null) {
1444
+ queuedCall = pendingStartCall;
1445
+ pendingStartCall = null;
1446
+ }
1447
+ }
1448
+
1449
+ if (queuedCall != null) {
1450
+ PluginCall finalQueuedCall = queuedCall;
1451
+ Log.d(TAG, "onCameraStopped: replaying pending start request");
1452
+ getBridge()
1453
+ .getActivity()
1454
+ .runOnUiThread(() -> start(finalQueuedCall));
1455
+ }
1456
+ }
1457
+
1458
+ private JSObject getViewSize(double x, double y, double width, double height) {
1459
+ JSObject ret = new JSObject();
1460
+ // Return values with proper rounding to avoid gaps
1461
+ // For positions (x, y): ceil to avoid gaps at top/left
1462
+ // For dimensions (width, height): floor to avoid gaps at bottom/right
1463
+ ret.put("x", Math.ceil(x));
1464
+ ret.put("y", Math.ceil(y));
1465
+ ret.put("width", Math.floor(width));
1466
+ ret.put("height", Math.floor(height));
1467
+ return ret;
1468
+ }
1469
+
1470
+ @Override
1471
+ public void onCameraStarted(int width, int height, int x, int y) {
1472
+ PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
1473
+ if (call != null) {
1474
+ // Convert pixel values back to logical units
1475
+ DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
1476
+ float pixelRatio = metrics.density;
1477
+
1478
+ // When WebView is offset from the top (e.g., below status bar),
1479
+ // we need to convert between JavaScript coordinates (relative to WebView)
1480
+ // and native coordinates (relative to screen)
1481
+ WebView webView = getBridge().getWebView();
1482
+ int webViewTopInset = 0;
1483
+ boolean isEdgeToEdgeActive = false;
1484
+ if (webView != null) {
1485
+ int[] location = new int[2];
1486
+ webView.getLocationOnScreen(location);
1487
+ webViewTopInset = location[1];
1488
+ isEdgeToEdgeActive = webViewTopInset > 0;
1489
+ }
1490
+
1491
+ // Only convert to relative position if edge-to-edge is active
1492
+ int relativeY = isEdgeToEdgeActive ? (y - webViewTopInset) : y;
1493
+
1494
+ Log.d("CameraPreview", "========================");
1495
+ Log.d("CameraPreview", "CAMERA STARTED - POSITION RETURNED:");
1496
+ Log.d("CameraPreview", "7. RETURNED (pixels) - x=" + x + ", y=" + y + ", width=" + width + ", height=" + height);
1497
+ Log.d("CameraPreview", "8. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE" : "INACTIVE"));
1498
+ Log.d("CameraPreview", "9. WEBVIEW INSET - " + webViewTopInset);
1499
+ Log.d(
1500
+ "CameraPreview",
1501
+ "10. RELATIVE Y - " + relativeY + " (y=" + y + (isEdgeToEdgeActive ? " - inset=" + webViewTopInset : " unchanged") + ")"
1502
+ );
1503
+ Log.d(
1504
+ "CameraPreview",
1505
+ "11. RETURNED (logical) - x=" +
1506
+ (x / pixelRatio) +
1507
+ ", y=" +
1508
+ (relativeY / pixelRatio) +
1509
+ ", width=" +
1510
+ (width / pixelRatio) +
1511
+ ", height=" +
1512
+ (height / pixelRatio)
1513
+ );
1514
+ Log.d("CameraPreview", "12. PIXEL RATIO - " + pixelRatio);
1515
+ Log.d("CameraPreview", "========================");
1516
+
1517
+ // Calculate logical values with proper rounding to avoid sub-pixel issues
1518
+ double logicalWidth = width / pixelRatio;
1519
+ double logicalHeight = height / pixelRatio;
1520
+ double logicalX = x / pixelRatio;
1521
+ double logicalY = relativeY / pixelRatio;
1522
+
1523
+ JSObject result = getViewSize(logicalX, logicalY, logicalWidth, logicalHeight);
1524
+
1525
+ // Log exact calculations to debug one-pixel difference
1526
+ Log.d("CameraPreview", "========================");
1527
+ Log.d("CameraPreview", "FINAL POSITION CALCULATIONS:");
1528
+ Log.d("CameraPreview", "Pixel values: x=" + x + ", y=" + relativeY + ", width=" + width + ", height=" + height);
1529
+ Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
1530
+ Log.d(
1531
+ "CameraPreview",
1532
+ "Logical values (exact): x=" + logicalX + ", y=" + logicalY + ", width=" + logicalWidth + ", height=" + logicalHeight
1533
+ );
1534
+ Log.d(
1535
+ "CameraPreview",
1536
+ "Logical values (rounded): x=" +
1537
+ Math.round(logicalX) +
1538
+ ", y=" +
1539
+ Math.round(logicalY) +
1540
+ ", width=" +
1541
+ Math.round(logicalWidth) +
1542
+ ", height=" +
1543
+ Math.round(logicalHeight)
1693
1544
  );
1694
- }
1695
1545
 
1696
- Log.d(
1697
- TAG,
1698
- "================================================================================"
1699
- );
1700
- });
1701
- });
1702
- }
1703
-
1704
- /**
1705
- * Compute a canonical orientation string matching iOS values:
1706
- * "portrait", "portrait-upside-down", "landscape-left", "landscape-right", or "unknown".
1707
- * Uses display rotation when available, with a fallback to configuration orientation.
1708
- */
1709
- private String getDeviceOrientationString() {
1710
- try {
1711
- int rotation = -1;
1712
- // Try to obtain display rotation in a backward/forward-compatible way
1713
- if (android.os.Build.VERSION.SDK_INT >= 30) {
1714
- android.view.Display display = getBridge().getActivity().getDisplay();
1715
- if (display != null) {
1716
- rotation = display.getRotation();
1717
- }
1718
- } else {
1719
- android.view.Display display = getBridge()
1720
- .getActivity()
1721
- .getWindowManager()
1722
- .getDefaultDisplay();
1723
- if (display != null) {
1724
- rotation = display.getRotation();
1725
- }
1726
- }
1727
-
1728
- if (rotation == android.view.Surface.ROTATION_0) {
1729
- return "portrait";
1730
- } else if (rotation == android.view.Surface.ROTATION_90) {
1731
- return "landscape-right";
1732
- } else if (rotation == android.view.Surface.ROTATION_180) {
1733
- return "portrait-upside-down";
1734
- } else if (rotation == android.view.Surface.ROTATION_270) {
1735
- return "landscape-left";
1736
- }
1737
-
1738
- // Fallback to configuration if rotation unavailable
1739
- int orientation = getContext()
1740
- .getResources()
1741
- .getConfiguration()
1742
- .orientation;
1743
- if (orientation == Configuration.ORIENTATION_PORTRAIT) return "portrait";
1744
- if (
1745
- orientation == Configuration.ORIENTATION_LANDSCAPE
1746
- ) return "landscape-right"; // default, avoid generic
1747
- return "unknown";
1748
- } catch (Throwable t) {
1749
- Log.w(TAG, "Failed to get precise orientation, falling back: " + t);
1750
- int orientation = getContext()
1751
- .getResources()
1752
- .getConfiguration()
1753
- .orientation;
1754
- if (orientation == Configuration.ORIENTATION_PORTRAIT) return "portrait";
1755
- if (
1756
- orientation == Configuration.ORIENTATION_LANDSCAPE
1757
- ) return "landscape-right"; // default, avoid generic
1758
- return "unknown";
1759
- }
1760
- }
1761
-
1762
- @Override
1763
- public void onPictureTaken(String base64, JSONObject exif) {
1764
- PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
1765
- if (pluginCall == null) {
1766
- Log.e("CameraPreview", "onPictureTaken: captureCallbackId is null");
1767
- return;
1768
- }
1769
- JSObject result = new JSObject();
1770
- result.put("value", base64);
1771
- result.put("exif", exif);
1772
- pluginCall.resolve(result);
1773
- bridge.releaseCall(pluginCall);
1774
- }
1775
-
1776
- @Override
1777
- public void onPictureTakenError(String message) {
1778
- PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
1779
- if (pluginCall == null) {
1780
- Log.e("CameraPreview", "onPictureTakenError: captureCallbackId is null");
1781
- return;
1782
- }
1783
- pluginCall.reject(message);
1784
- bridge.releaseCall(pluginCall);
1785
- }
1786
-
1787
- @Override
1788
- public void onCameraStopped(CameraXView source) {
1789
- if (cameraXView != null && cameraXView != source) {
1790
- Log.d(TAG, "onCameraStopped: ignoring callback from stale instance");
1791
- return;
1792
- }
1793
- // Ensure reference is cleared once the originating CameraXView has fully stopped
1794
- if (cameraXView == source) {
1795
- cameraXView = null;
1796
- }
1797
-
1798
- PluginCall queuedCall = null;
1799
- synchronized (pendingStartLock) {
1800
- if (pendingStartCall != null) {
1801
- queuedCall = pendingStartCall;
1802
- pendingStartCall = null;
1803
- }
1804
- }
1805
-
1806
- if (queuedCall != null) {
1807
- PluginCall finalQueuedCall = queuedCall;
1808
- Log.d(TAG, "onCameraStopped: replaying pending start request");
1809
- getBridge().getActivity().runOnUiThread(() -> start(finalQueuedCall));
1810
- }
1811
- }
1812
-
1813
- private JSObject getViewSize(
1814
- double x,
1815
- double y,
1816
- double width,
1817
- double height
1818
- ) {
1819
- JSObject ret = new JSObject();
1820
- // Return values with proper rounding to avoid gaps
1821
- // For positions (x, y): ceil to avoid gaps at top/left
1822
- // For dimensions (width, height): floor to avoid gaps at bottom/right
1823
- ret.put("x", Math.ceil(x));
1824
- ret.put("y", Math.ceil(y));
1825
- ret.put("width", Math.floor(width));
1826
- ret.put("height", Math.floor(height));
1827
- return ret;
1828
- }
1829
-
1830
- @Override
1831
- public void onCameraStarted(int width, int height, int x, int y) {
1832
- PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
1833
- if (call != null) {
1834
- // Convert pixel values back to logical units
1835
- DisplayMetrics metrics = getBridge()
1836
- .getActivity()
1837
- .getResources()
1838
- .getDisplayMetrics();
1839
- float pixelRatio = metrics.density;
1840
-
1841
- // When WebView is offset from the top (e.g., below status bar),
1842
- // we need to convert between JavaScript coordinates (relative to WebView)
1843
- // and native coordinates (relative to screen)
1844
- WebView webView = getBridge().getWebView();
1845
- int webViewTopInset = 0;
1846
- boolean isEdgeToEdgeActive = false;
1847
- if (webView != null) {
1848
- int[] location = new int[2];
1849
- webView.getLocationOnScreen(location);
1850
- webViewTopInset = location[1];
1851
- isEdgeToEdgeActive = webViewTopInset > 0;
1852
- }
1853
-
1854
- // Only convert to relative position if edge-to-edge is active
1855
- int relativeY = isEdgeToEdgeActive ? (y - webViewTopInset) : y;
1856
-
1857
- Log.d("CameraPreview", "========================");
1858
- Log.d("CameraPreview", "CAMERA STARTED - POSITION RETURNED:");
1859
- Log.d(
1860
- "CameraPreview",
1861
- "7. RETURNED (pixels) - x=" +
1862
- x +
1863
- ", y=" +
1864
- y +
1865
- ", width=" +
1866
- width +
1867
- ", height=" +
1868
- height
1869
- );
1870
- Log.d(
1871
- "CameraPreview",
1872
- "8. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE" : "INACTIVE")
1873
- );
1874
- Log.d("CameraPreview", "9. WEBVIEW INSET - " + webViewTopInset);
1875
- Log.d(
1876
- "CameraPreview",
1877
- "10. RELATIVE Y - " +
1878
- relativeY +
1879
- " (y=" +
1880
- y +
1881
- (isEdgeToEdgeActive ? " - inset=" + webViewTopInset : " unchanged") +
1882
- ")"
1883
- );
1884
- Log.d(
1885
- "CameraPreview",
1886
- "11. RETURNED (logical) - x=" +
1887
- (x / pixelRatio) +
1888
- ", y=" +
1889
- (relativeY / pixelRatio) +
1890
- ", width=" +
1891
- (width / pixelRatio) +
1892
- ", height=" +
1893
- (height / pixelRatio)
1894
- );
1895
- Log.d("CameraPreview", "12. PIXEL RATIO - " + pixelRatio);
1896
- Log.d("CameraPreview", "========================");
1897
-
1898
- // Calculate logical values with proper rounding to avoid sub-pixel issues
1899
- double logicalWidth = width / pixelRatio;
1900
- double logicalHeight = height / pixelRatio;
1901
- double logicalX = x / pixelRatio;
1902
- double logicalY = relativeY / pixelRatio;
1903
-
1904
- JSObject result = getViewSize(
1905
- logicalX,
1906
- logicalY,
1907
- logicalWidth,
1908
- logicalHeight
1909
- );
1910
-
1911
- // Log exact calculations to debug one-pixel difference
1912
- Log.d("CameraPreview", "========================");
1913
- Log.d("CameraPreview", "FINAL POSITION CALCULATIONS:");
1914
- Log.d(
1915
- "CameraPreview",
1916
- "Pixel values: x=" +
1917
- x +
1918
- ", y=" +
1919
- relativeY +
1920
- ", width=" +
1921
- width +
1922
- ", height=" +
1923
- height
1924
- );
1925
- Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
1926
- Log.d(
1927
- "CameraPreview",
1928
- "Logical values (exact): x=" +
1929
- logicalX +
1930
- ", y=" +
1931
- logicalY +
1932
- ", width=" +
1933
- logicalWidth +
1934
- ", height=" +
1935
- logicalHeight
1936
- );
1937
- Log.d(
1938
- "CameraPreview",
1939
- "Logical values (rounded): x=" +
1940
- Math.round(logicalX) +
1941
- ", y=" +
1942
- Math.round(logicalY) +
1943
- ", width=" +
1944
- Math.round(logicalWidth) +
1945
- ", height=" +
1946
- Math.round(logicalHeight)
1947
- );
1948
-
1949
- // Check if previewContainer has any padding or margin that might cause offset
1950
- if (cameraXView != null) {
1951
- View previewContainer = cameraXView.getPreviewContainer();
1952
- if (previewContainer != null) {
1953
- Log.d(
1954
- "CameraPreview",
1955
- "PreviewContainer padding: left=" +
1956
- previewContainer.getPaddingLeft() +
1957
- ", top=" +
1958
- previewContainer.getPaddingTop() +
1959
- ", right=" +
1960
- previewContainer.getPaddingRight() +
1961
- ", bottom=" +
1962
- previewContainer.getPaddingBottom()
1963
- );
1964
- ViewGroup.LayoutParams params = previewContainer.getLayoutParams();
1965
- if (params instanceof ViewGroup.MarginLayoutParams) {
1966
- ViewGroup.MarginLayoutParams marginParams =
1967
- (ViewGroup.MarginLayoutParams) params;
1546
+ // Check if previewContainer has any padding or margin that might cause offset
1547
+ if (cameraXView != null) {
1548
+ View previewContainer = cameraXView.getPreviewContainer();
1549
+ if (previewContainer != null) {
1550
+ Log.d(
1551
+ "CameraPreview",
1552
+ "PreviewContainer padding: left=" +
1553
+ previewContainer.getPaddingLeft() +
1554
+ ", top=" +
1555
+ previewContainer.getPaddingTop() +
1556
+ ", right=" +
1557
+ previewContainer.getPaddingRight() +
1558
+ ", bottom=" +
1559
+ previewContainer.getPaddingBottom()
1560
+ );
1561
+ ViewGroup.LayoutParams params = previewContainer.getLayoutParams();
1562
+ if (params instanceof ViewGroup.MarginLayoutParams) {
1563
+ ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) params;
1564
+ Log.d(
1565
+ "CameraPreview",
1566
+ "PreviewContainer margins: left=" +
1567
+ marginParams.leftMargin +
1568
+ ", top=" +
1569
+ marginParams.topMargin +
1570
+ ", right=" +
1571
+ marginParams.rightMargin +
1572
+ ", bottom=" +
1573
+ marginParams.bottomMargin
1574
+ );
1575
+ }
1576
+ }
1577
+ }
1578
+ Log.d("CameraPreview", "========================");
1579
+
1580
+ // Log what we're returning
1968
1581
  Log.d(
1969
- "CameraPreview",
1970
- "PreviewContainer margins: left=" +
1971
- marginParams.leftMargin +
1972
- ", top=" +
1973
- marginParams.topMargin +
1974
- ", right=" +
1975
- marginParams.rightMargin +
1976
- ", bottom=" +
1977
- marginParams.bottomMargin
1582
+ "CameraPreview",
1583
+ "Returning to JS - x: " +
1584
+ logicalX +
1585
+ " (from " +
1586
+ logicalX +
1587
+ "), y: " +
1588
+ logicalY +
1589
+ " (from " +
1590
+ logicalY +
1591
+ "), width: " +
1592
+ logicalWidth +
1593
+ " (from " +
1594
+ logicalWidth +
1595
+ "), height: " +
1596
+ logicalHeight +
1597
+ " (from " +
1598
+ logicalHeight +
1599
+ ")"
1978
1600
  );
1979
- }
1980
- }
1981
- }
1982
- Log.d("CameraPreview", "========================");
1983
-
1984
- // Log what we're returning
1985
- Log.d(
1986
- "CameraPreview",
1987
- "Returning to JS - x: " +
1988
- logicalX +
1989
- " (from " +
1990
- logicalX +
1991
- "), y: " +
1992
- logicalY +
1993
- " (from " +
1994
- logicalY +
1995
- "), width: " +
1996
- logicalWidth +
1997
- " (from " +
1998
- logicalWidth +
1999
- "), height: " +
2000
- logicalHeight +
2001
- " (from " +
2002
- logicalHeight +
2003
- ")"
2004
- );
2005
-
2006
- call.resolve(result);
2007
- bridge.releaseCall(call);
2008
- cameraStartCallbackId = null; // Prevent re-use
2009
- }
2010
- }
2011
-
2012
- @Override
2013
- public void onSampleTaken(String result) {
2014
- PluginCall call = bridge.getSavedCall(sampleCallbackId);
2015
- if (call != null) {
2016
- JSObject ret = new JSObject();
2017
- ret.put("value", result);
2018
- call.resolve(ret);
2019
- bridge.releaseCall(call);
2020
- sampleCallbackId = null;
2021
- } else {
2022
- Log.w("CameraPreview", "onSampleTaken: no pending call to resolve");
2023
- }
2024
- }
2025
-
2026
- @Override
2027
- public void onSampleTakenError(String message) {
2028
- PluginCall call = bridge.getSavedCall(sampleCallbackId);
2029
- if (call != null) {
2030
- call.reject(message);
2031
- bridge.releaseCall(call);
2032
- sampleCallbackId = null;
2033
- } else {
2034
- Log.e(
2035
- "CameraPreview",
2036
- "Sample taken error (no pending call): " + message
2037
- );
2038
- }
2039
- }
2040
-
2041
- @Override
2042
- public void onCameraStartError(String message) {
2043
- PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
2044
- if (call != null) {
2045
- call.reject(message);
2046
- bridge.releaseCall(call);
2047
- cameraStartCallbackId = null;
2048
- }
2049
- }
2050
-
2051
- @PluginMethod
2052
- public void setAspectRatio(PluginCall call) {
2053
- if (cameraXView == null || !cameraXView.isRunning()) {
2054
- call.reject("Camera is not running");
2055
- return;
2056
- }
2057
- String aspectRatio = call.getString("aspectRatio", "4:3");
2058
- Float x = call.getFloat("x");
2059
- Float y = call.getFloat("y");
2060
-
2061
- getActivity()
2062
- .runOnUiThread(() -> {
2063
- cameraXView.setAspectRatio(aspectRatio, x, y, () -> {
2064
- // Return the actual preview bounds after layout and camera operations are complete
2065
- int[] bounds = cameraXView.getCurrentPreviewBounds();
2066
- JSObject ret = new JSObject();
2067
- ret.put("x", bounds[0]);
2068
- ret.put("y", bounds[1]);
2069
- ret.put("width", bounds[2]);
2070
- ret.put("height", bounds[3]);
2071
- call.resolve(ret);
1601
+
1602
+ call.resolve(result);
1603
+ bridge.releaseCall(call);
1604
+ cameraStartCallbackId = null; // Prevent re-use
1605
+ }
1606
+ }
1607
+
1608
+ @Override
1609
+ public void onSampleTaken(String result) {
1610
+ PluginCall call = bridge.getSavedCall(sampleCallbackId);
1611
+ if (call != null) {
1612
+ JSObject ret = new JSObject();
1613
+ ret.put("value", result);
1614
+ call.resolve(ret);
1615
+ bridge.releaseCall(call);
1616
+ sampleCallbackId = null;
1617
+ } else {
1618
+ Log.w("CameraPreview", "onSampleTaken: no pending call to resolve");
1619
+ }
1620
+ }
1621
+
1622
+ @Override
1623
+ public void onSampleTakenError(String message) {
1624
+ PluginCall call = bridge.getSavedCall(sampleCallbackId);
1625
+ if (call != null) {
1626
+ call.reject(message);
1627
+ bridge.releaseCall(call);
1628
+ sampleCallbackId = null;
1629
+ } else {
1630
+ Log.e("CameraPreview", "Sample taken error (no pending call): " + message);
1631
+ }
1632
+ }
1633
+
1634
+ @Override
1635
+ public void onCameraStartError(String message) {
1636
+ PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
1637
+ if (call != null) {
1638
+ call.reject(message);
1639
+ bridge.releaseCall(call);
1640
+ cameraStartCallbackId = null;
1641
+ }
1642
+ }
1643
+
1644
+ @PluginMethod
1645
+ public void setAspectRatio(PluginCall call) {
1646
+ if (cameraXView == null || !cameraXView.isRunning()) {
1647
+ call.reject("Camera is not running");
1648
+ return;
1649
+ }
1650
+ String aspectRatio = call.getString("aspectRatio", "4:3");
1651
+ Float x = call.getFloat("x");
1652
+ Float y = call.getFloat("y");
1653
+
1654
+ getActivity().runOnUiThread(() -> {
1655
+ cameraXView.setAspectRatio(aspectRatio, x, y, () -> {
1656
+ // Return the actual preview bounds after layout and camera operations are complete
1657
+ int[] bounds = cameraXView.getCurrentPreviewBounds();
1658
+ JSObject ret = new JSObject();
1659
+ ret.put("x", bounds[0]);
1660
+ ret.put("y", bounds[1]);
1661
+ ret.put("width", bounds[2]);
1662
+ ret.put("height", bounds[3]);
1663
+ call.resolve(ret);
1664
+ });
2072
1665
  });
2073
- });
2074
- }
2075
-
2076
- @PluginMethod
2077
- public void getAspectRatio(PluginCall call) {
2078
- if (cameraXView == null || !cameraXView.isRunning()) {
2079
- call.reject("Camera is not running");
2080
- return;
2081
- }
2082
- String aspectRatio = cameraXView.getAspectRatio();
2083
- JSObject ret = new JSObject();
2084
- ret.put("aspectRatio", aspectRatio);
2085
- call.resolve(ret);
2086
- }
2087
-
2088
- @PluginMethod
2089
- public void setGridMode(PluginCall call) {
2090
- if (cameraXView == null || !cameraXView.isRunning()) {
2091
- call.reject("Camera is not running");
2092
- return;
2093
- }
2094
- String gridMode = call.getString("gridMode", "none");
2095
- getActivity()
2096
- .runOnUiThread(() -> {
2097
- cameraXView.setGridMode(gridMode);
2098
- call.resolve();
2099
- });
2100
- }
2101
-
2102
- @PluginMethod
2103
- public void getGridMode(PluginCall call) {
2104
- if (cameraXView == null || !cameraXView.isRunning()) {
2105
- call.reject("Camera is not running");
2106
- return;
2107
- }
2108
- JSObject ret = new JSObject();
2109
- ret.put("gridMode", cameraXView.getGridMode());
2110
- call.resolve(ret);
2111
- }
2112
-
2113
- @PluginMethod
2114
- public void getPreviewSize(PluginCall call) {
2115
- if (cameraXView == null || !cameraXView.isRunning()) {
2116
- call.reject("Camera is not running");
2117
- return;
2118
- }
2119
-
2120
- // Convert pixel values back to logical units
2121
- DisplayMetrics metrics = getBridge()
2122
- .getActivity()
2123
- .getResources()
2124
- .getDisplayMetrics();
2125
- float pixelRatio = metrics.density;
2126
-
2127
- JSObject ret = new JSObject();
2128
- // Use same rounding strategy as start method
2129
- double x = Math.ceil(cameraXView.getPreviewX() / pixelRatio);
2130
- double y = Math.ceil(cameraXView.getPreviewY() / pixelRatio);
2131
- double width = Math.floor(cameraXView.getPreviewWidth() / pixelRatio);
2132
- double height = Math.floor(cameraXView.getPreviewHeight() / pixelRatio);
2133
-
2134
- Log.d(
2135
- "CameraPreview",
2136
- "getPreviewSize: x=" +
2137
- x +
2138
- ", y=" +
2139
- y +
2140
- ", width=" +
2141
- width +
2142
- ", height=" +
2143
- height
2144
- );
2145
- ret.put("x", x);
2146
- ret.put("y", y);
2147
- ret.put("width", width);
2148
- ret.put("height", height);
2149
- call.resolve(ret);
2150
- }
2151
-
2152
- @PluginMethod
2153
- public void setPreviewSize(PluginCall call) {
2154
- if (cameraXView == null || !cameraXView.isRunning()) {
2155
- call.reject("Camera is not running");
2156
- return;
2157
- }
2158
-
2159
- // Get values from call - null values will become 0
2160
- Integer xParam = call.getInt("x");
2161
- Integer yParam = call.getInt("y");
2162
- Integer widthParam = call.getInt("width");
2163
- Integer heightParam = call.getInt("height");
2164
-
2165
- // Apply pixel ratio conversion to non-null values
2166
- DisplayMetrics metrics = getBridge()
2167
- .getActivity()
2168
- .getResources()
2169
- .getDisplayMetrics();
2170
- float pixelRatio = metrics.density;
2171
-
2172
- // Check if edge-to-edge mode is active
2173
- WebView webView = getBridge().getWebView();
2174
- int webViewTopInset = 0;
2175
- boolean isEdgeToEdgeActive = false;
2176
- if (webView != null) {
2177
- int[] location = new int[2];
2178
- webView.getLocationOnScreen(location);
2179
- webViewTopInset = location[1];
2180
- isEdgeToEdgeActive = webViewTopInset > 0;
2181
- }
2182
-
2183
- int x = (xParam != null && xParam > 0) ? (int) (xParam * pixelRatio) : 0;
2184
- int y = (yParam != null && yParam > 0) ? (int) (yParam * pixelRatio) : 0;
2185
-
2186
- // Add edge-to-edge inset to Y if active
2187
- if (isEdgeToEdgeActive && y > 0) {
2188
- y += webViewTopInset;
2189
- }
2190
- int width = (widthParam != null && widthParam > 0)
2191
- ? (int) (widthParam * pixelRatio)
2192
- : 0;
2193
- int height = (heightParam != null && heightParam > 0)
2194
- ? (int) (heightParam * pixelRatio)
2195
- : 0;
2196
-
2197
- cameraXView.setPreviewSize(x, y, width, height, () -> {
2198
- // Return the actual preview bounds after layout operations are complete
2199
- int[] bounds = cameraXView.getCurrentPreviewBounds();
2200
- JSObject ret = new JSObject();
2201
- ret.put("x", bounds[0]);
2202
- ret.put("y", bounds[1]);
2203
- ret.put("width", bounds[2]);
2204
- ret.put("height", bounds[3]);
2205
- call.resolve(ret);
2206
- });
2207
- }
2208
-
2209
- @PluginMethod
2210
- public void deleteFile(PluginCall call) {
2211
- String path = call.getString("path");
2212
- if (path == null || path.isEmpty()) {
2213
- call.reject("path parameter is required");
2214
- return;
2215
- }
2216
- try {
2217
- java.io.File f = new java.io.File(
2218
- Objects.requireNonNull(Uri.parse(path).getPath())
2219
- );
2220
- boolean deleted = f.exists() && f.delete();
2221
- JSObject ret = new JSObject();
2222
- ret.put("success", deleted);
2223
- call.resolve(ret);
2224
- } catch (Exception e) {
2225
- call.reject("Failed to delete file: " + e.getMessage());
2226
- }
2227
- }
2228
-
2229
- @PluginMethod
2230
- public void startRecordVideo(PluginCall call) {
2231
- if (cameraXView == null || !cameraXView.isRunning()) {
2232
- call.reject("Camera is not running");
2233
- return;
2234
- }
2235
-
2236
- boolean disableAudio = call.getBoolean("disableAudio") != null
2237
- ? Boolean.TRUE.equals(call.getBoolean("disableAudio"))
2238
- : this.lastDisableAudio;
2239
- this.lastDisableAudio = disableAudio;
2240
- String permissionAlias = disableAudio
2241
- ? CAMERA_ONLY_PERMISSION_ALIAS
2242
- : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
2243
-
2244
- if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
2245
- try {
2246
- cameraXView.startRecordVideo();
2247
- call.resolve();
2248
- } catch (Exception e) {
2249
- call.reject("Failed to start video recording: " + e.getMessage());
2250
- }
2251
- } else {
2252
- requestPermissionForAlias(
2253
- permissionAlias,
2254
- call,
2255
- "handleVideoRecordingPermissionResult"
2256
- );
2257
- }
2258
- }
2259
-
2260
- @PluginMethod
2261
- public void stopRecordVideo(PluginCall call) {
2262
- if (cameraXView == null || !cameraXView.isRunning()) {
2263
- call.reject("Camera is not running");
2264
- return;
2265
- }
2266
-
2267
- try {
2268
- bridge.saveCall(call);
2269
- final String cbId = call.getCallbackId();
2270
- cameraXView.stopRecordVideo(
2271
- new CameraXView.VideoRecordingCallback() {
2272
- @Override
2273
- public void onSuccess(String filePath) {
2274
- PluginCall saved = bridge.getSavedCall(cbId);
2275
- if (saved != null) {
2276
- JSObject result = new JSObject();
2277
- result.put("videoFilePath", filePath);
2278
- saved.resolve(result);
2279
- bridge.releaseCall(saved);
2280
- }
2281
- }
2282
-
2283
- @Override
2284
- public void onError(String message) {
2285
- PluginCall saved = bridge.getSavedCall(cbId);
2286
- if (saved != null) {
2287
- saved.reject("Failed to stop video recording: " + message);
2288
- bridge.releaseCall(saved);
1666
+ }
1667
+
1668
+ @PluginMethod
1669
+ public void getAspectRatio(PluginCall call) {
1670
+ if (cameraXView == null || !cameraXView.isRunning()) {
1671
+ call.reject("Camera is not running");
1672
+ return;
1673
+ }
1674
+ String aspectRatio = cameraXView.getAspectRatio();
1675
+ JSObject ret = new JSObject();
1676
+ ret.put("aspectRatio", aspectRatio);
1677
+ call.resolve(ret);
1678
+ }
1679
+
1680
+ @PluginMethod
1681
+ public void setGridMode(PluginCall call) {
1682
+ if (cameraXView == null || !cameraXView.isRunning()) {
1683
+ call.reject("Camera is not running");
1684
+ return;
1685
+ }
1686
+ String gridMode = call.getString("gridMode", "none");
1687
+ getActivity().runOnUiThread(() -> {
1688
+ cameraXView.setGridMode(gridMode);
1689
+ call.resolve();
1690
+ });
1691
+ }
1692
+
1693
+ @PluginMethod
1694
+ public void getGridMode(PluginCall call) {
1695
+ if (cameraXView == null || !cameraXView.isRunning()) {
1696
+ call.reject("Camera is not running");
1697
+ return;
1698
+ }
1699
+ JSObject ret = new JSObject();
1700
+ ret.put("gridMode", cameraXView.getGridMode());
1701
+ call.resolve(ret);
1702
+ }
1703
+
1704
+ @PluginMethod
1705
+ public void getPreviewSize(PluginCall call) {
1706
+ if (cameraXView == null || !cameraXView.isRunning()) {
1707
+ call.reject("Camera is not running");
1708
+ return;
1709
+ }
1710
+
1711
+ // Convert pixel values back to logical units
1712
+ DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
1713
+ float pixelRatio = metrics.density;
1714
+
1715
+ JSObject ret = new JSObject();
1716
+ // Use same rounding strategy as start method
1717
+ double x = Math.ceil(cameraXView.getPreviewX() / pixelRatio);
1718
+ double y = Math.ceil(cameraXView.getPreviewY() / pixelRatio);
1719
+ double width = Math.floor(cameraXView.getPreviewWidth() / pixelRatio);
1720
+ double height = Math.floor(cameraXView.getPreviewHeight() / pixelRatio);
1721
+
1722
+ Log.d("CameraPreview", "getPreviewSize: x=" + x + ", y=" + y + ", width=" + width + ", height=" + height);
1723
+ ret.put("x", x);
1724
+ ret.put("y", y);
1725
+ ret.put("width", width);
1726
+ ret.put("height", height);
1727
+ call.resolve(ret);
1728
+ }
1729
+
1730
+ @PluginMethod
1731
+ public void setPreviewSize(PluginCall call) {
1732
+ if (cameraXView == null || !cameraXView.isRunning()) {
1733
+ call.reject("Camera is not running");
1734
+ return;
1735
+ }
1736
+
1737
+ // Get values from call - null values will become 0
1738
+ Integer xParam = call.getInt("x");
1739
+ Integer yParam = call.getInt("y");
1740
+ Integer widthParam = call.getInt("width");
1741
+ Integer heightParam = call.getInt("height");
1742
+
1743
+ // Apply pixel ratio conversion to non-null values
1744
+ DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
1745
+ float pixelRatio = metrics.density;
1746
+
1747
+ // Check if edge-to-edge mode is active
1748
+ WebView webView = getBridge().getWebView();
1749
+ int webViewTopInset = 0;
1750
+ boolean isEdgeToEdgeActive = false;
1751
+ if (webView != null) {
1752
+ int[] location = new int[2];
1753
+ webView.getLocationOnScreen(location);
1754
+ webViewTopInset = location[1];
1755
+ isEdgeToEdgeActive = webViewTopInset > 0;
1756
+ }
1757
+
1758
+ int x = (xParam != null && xParam > 0) ? (int) (xParam * pixelRatio) : 0;
1759
+ int y = (yParam != null && yParam > 0) ? (int) (yParam * pixelRatio) : 0;
1760
+
1761
+ // Add edge-to-edge inset to Y if active
1762
+ if (isEdgeToEdgeActive && y > 0) {
1763
+ y += webViewTopInset;
1764
+ }
1765
+ int width = (widthParam != null && widthParam > 0) ? (int) (widthParam * pixelRatio) : 0;
1766
+ int height = (heightParam != null && heightParam > 0) ? (int) (heightParam * pixelRatio) : 0;
1767
+
1768
+ cameraXView.setPreviewSize(x, y, width, height, () -> {
1769
+ // Return the actual preview bounds after layout operations are complete
1770
+ int[] bounds = cameraXView.getCurrentPreviewBounds();
1771
+ JSObject ret = new JSObject();
1772
+ ret.put("x", bounds[0]);
1773
+ ret.put("y", bounds[1]);
1774
+ ret.put("width", bounds[2]);
1775
+ ret.put("height", bounds[3]);
1776
+ call.resolve(ret);
1777
+ });
1778
+ }
1779
+
1780
+ @PluginMethod
1781
+ public void deleteFile(PluginCall call) {
1782
+ String path = call.getString("path");
1783
+ if (path == null || path.isEmpty()) {
1784
+ call.reject("path parameter is required");
1785
+ return;
1786
+ }
1787
+ try {
1788
+ java.io.File f = new java.io.File(Objects.requireNonNull(Uri.parse(path).getPath()));
1789
+ boolean deleted = f.exists() && f.delete();
1790
+ JSObject ret = new JSObject();
1791
+ ret.put("success", deleted);
1792
+ call.resolve(ret);
1793
+ } catch (Exception e) {
1794
+ call.reject("Failed to delete file: " + e.getMessage());
1795
+ }
1796
+ }
1797
+
1798
+ @PluginMethod
1799
+ public void startRecordVideo(PluginCall call) {
1800
+ if (cameraXView == null || !cameraXView.isRunning()) {
1801
+ call.reject("Camera is not running");
1802
+ return;
1803
+ }
1804
+
1805
+ boolean disableAudio = call.getBoolean("disableAudio") != null
1806
+ ? Boolean.TRUE.equals(call.getBoolean("disableAudio"))
1807
+ : this.lastDisableAudio;
1808
+ this.lastDisableAudio = disableAudio;
1809
+ String permissionAlias = disableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
1810
+
1811
+ if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
1812
+ try {
1813
+ cameraXView.startRecordVideo();
1814
+ call.resolve();
1815
+ } catch (Exception e) {
1816
+ call.reject("Failed to start video recording: " + e.getMessage());
2289
1817
  }
2290
- }
2291
- }
2292
- );
2293
- } catch (Exception e) {
2294
- call.reject("Failed to stop video recording: " + e.getMessage());
2295
- }
2296
- }
2297
-
2298
- @PermissionCallback
2299
- private void handleVideoRecordingPermissionResult(PluginCall call) {
2300
- // Use the persisted session value to determine which permission we requested
2301
- String permissionAlias = this.lastDisableAudio
2302
- ? CAMERA_ONLY_PERMISSION_ALIAS
2303
- : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
2304
-
2305
- // Check if either permission is granted (mirroring handleCameraPermissionResult)
2306
- if (
2307
- PermissionState.GRANTED.equals(
2308
- getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
2309
- ) ||
2310
- PermissionState.GRANTED.equals(
2311
- getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS)
2312
- )
2313
- ) {
2314
- try {
2315
- cameraXView.startRecordVideo();
2316
- call.resolve();
2317
- } catch (Exception e) {
2318
- call.reject("Failed to start video recording: " + e.getMessage());
2319
- }
2320
- } else {
2321
- call.reject(
2322
- "camera permission denied. enable camera access in Settings.",
2323
- "cameraPermissionDenied"
2324
- );
2325
- }
2326
- }
2327
-
2328
- @PluginMethod
2329
- public void getSafeAreaInsets(PluginCall call) {
2330
- JSObject ret = new JSObject();
2331
- int orientation = getContext()
2332
- .getResources()
2333
- .getConfiguration()
2334
- .orientation;
2335
-
2336
- int notchInsetPx = 0;
2337
-
2338
- try {
2339
- View decorView = getBridge().getActivity().getWindow().getDecorView();
2340
- WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
2341
-
2342
- if (insets != null) {
2343
- // Get display cutout insets (notch, punch hole, etc.)
2344
- // this.Capacitor.Plugins.CameraPreview.getSafeAreaInsets()
2345
- Insets cutout = insets.getInsets(
2346
- WindowInsetsCompat.Type.displayCutout()
2347
- );
1818
+ } else {
1819
+ requestPermissionForAlias(permissionAlias, call, "handleVideoRecordingPermissionResult");
1820
+ }
1821
+ }
1822
+
1823
+ @PluginMethod
1824
+ public void stopRecordVideo(PluginCall call) {
1825
+ if (cameraXView == null || !cameraXView.isRunning()) {
1826
+ call.reject("Camera is not running");
1827
+ return;
1828
+ }
2348
1829
 
2349
- // Get system bars insets (status bar, navigation bars)
2350
- Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
2351
-
2352
- // In portrait mode, notch is at the top
2353
- // In landscape mode, notch is typically at the left side (or right, but left is more common)
2354
- if (orientation == Configuration.ORIENTATION_PORTRAIT) {
2355
- // Portrait: return top inset (notch/status bar)
2356
- notchInsetPx = Math.max(cutout.top, sysBars.top);
2357
- } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
2358
- // Landscape: return left inset (notch moved to side)
2359
- notchInsetPx = Math.max(cutout.left, sysBars.left);
2360
- // Additional fallback: some devices might have the notch on the right in landscape
2361
- // If left is 0, check right side as well
2362
- if (notchInsetPx == 0) {
2363
- notchInsetPx = Math.max(cutout.right, sysBars.right);
2364
- }
1830
+ try {
1831
+ bridge.saveCall(call);
1832
+ final String cbId = call.getCallbackId();
1833
+ cameraXView.stopRecordVideo(
1834
+ new CameraXView.VideoRecordingCallback() {
1835
+ @Override
1836
+ public void onSuccess(String filePath) {
1837
+ PluginCall saved = bridge.getSavedCall(cbId);
1838
+ if (saved != null) {
1839
+ JSObject result = new JSObject();
1840
+ result.put("videoFilePath", filePath);
1841
+ saved.resolve(result);
1842
+ bridge.releaseCall(saved);
1843
+ }
1844
+ }
1845
+
1846
+ @Override
1847
+ public void onError(String message) {
1848
+ PluginCall saved = bridge.getSavedCall(cbId);
1849
+ if (saved != null) {
1850
+ saved.reject("Failed to stop video recording: " + message);
1851
+ bridge.releaseCall(saved);
1852
+ }
1853
+ }
1854
+ }
1855
+ );
1856
+ } catch (Exception e) {
1857
+ call.reject("Failed to stop video recording: " + e.getMessage());
1858
+ }
1859
+ }
1860
+
1861
+ @PermissionCallback
1862
+ private void handleVideoRecordingPermissionResult(PluginCall call) {
1863
+ // Use the persisted session value to determine which permission we requested
1864
+ String permissionAlias = this.lastDisableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
1865
+
1866
+ // Check if either permission is granted (mirroring handleCameraPermissionResult)
1867
+ if (
1868
+ PermissionState.GRANTED.equals(getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)) ||
1869
+ PermissionState.GRANTED.equals(getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS))
1870
+ ) {
1871
+ try {
1872
+ cameraXView.startRecordVideo();
1873
+ call.resolve();
1874
+ } catch (Exception e) {
1875
+ call.reject("Failed to start video recording: " + e.getMessage());
1876
+ }
2365
1877
  } else {
2366
- // Unknown orientation, default to top
2367
- notchInsetPx = Math.max(cutout.top, sysBars.top);
2368
- }
2369
- } else {
2370
- // Fallback to status bar height if WindowInsets are not available
2371
- notchInsetPx = getStatusBarHeightPx();
2372
- }
2373
- } catch (Exception e) {
2374
- // Final fallback
2375
- notchInsetPx = getStatusBarHeightPx();
2376
- }
2377
-
2378
- // Convert pixels to dp for consistency with JS layout units
2379
- float density = getContext().getResources().getDisplayMetrics().density;
2380
- ret.put("orientation", orientation);
2381
- ret.put("top", notchInsetPx / density);
2382
- call.resolve(ret);
2383
- }
2384
-
2385
- private int getStatusBarHeightPx() {
2386
- int result = 0;
2387
- @SuppressLint("InternalInsetResource")
2388
- int resourceId = getContext()
2389
- .getResources()
2390
- .getIdentifier("status_bar_height", "dimen", "android");
2391
- if (resourceId > 0) {
2392
- result = getContext().getResources().getDimensionPixelSize(resourceId);
2393
- }
2394
- return result;
2395
- }
1878
+ call.reject("camera permission denied. enable camera access in Settings.", "cameraPermissionDenied");
1879
+ }
1880
+ }
1881
+
1882
+ @PluginMethod
1883
+ public void getSafeAreaInsets(PluginCall call) {
1884
+ JSObject ret = new JSObject();
1885
+ int orientation = getContext().getResources().getConfiguration().orientation;
1886
+
1887
+ int notchInsetPx = 0;
1888
+
1889
+ try {
1890
+ View decorView = getBridge().getActivity().getWindow().getDecorView();
1891
+ WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
1892
+
1893
+ if (insets != null) {
1894
+ // Get display cutout insets (notch, punch hole, etc.)
1895
+ // this.Capacitor.Plugins.CameraPreview.getSafeAreaInsets()
1896
+ Insets cutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout());
1897
+
1898
+ // Get system bars insets (status bar, navigation bars)
1899
+ Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
1900
+
1901
+ // In portrait mode, notch is at the top
1902
+ // In landscape mode, notch is typically at the left side (or right, but left is more common)
1903
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
1904
+ // Portrait: return top inset (notch/status bar)
1905
+ notchInsetPx = Math.max(cutout.top, sysBars.top);
1906
+ } else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
1907
+ // Landscape: return left inset (notch moved to side)
1908
+ notchInsetPx = Math.max(cutout.left, sysBars.left);
1909
+ // Additional fallback: some devices might have the notch on the right in landscape
1910
+ // If left is 0, check right side as well
1911
+ if (notchInsetPx == 0) {
1912
+ notchInsetPx = Math.max(cutout.right, sysBars.right);
1913
+ }
1914
+ } else {
1915
+ // Unknown orientation, default to top
1916
+ notchInsetPx = Math.max(cutout.top, sysBars.top);
1917
+ }
1918
+ } else {
1919
+ // Fallback to status bar height if WindowInsets are not available
1920
+ notchInsetPx = getStatusBarHeightPx();
1921
+ }
1922
+ } catch (Exception e) {
1923
+ // Final fallback
1924
+ notchInsetPx = getStatusBarHeightPx();
1925
+ }
1926
+
1927
+ // Convert pixels to dp for consistency with JS layout units
1928
+ float density = getContext().getResources().getDisplayMetrics().density;
1929
+ ret.put("orientation", orientation);
1930
+ ret.put("top", notchInsetPx / density);
1931
+ call.resolve(ret);
1932
+ }
1933
+
1934
+ private int getStatusBarHeightPx() {
1935
+ int result = 0;
1936
+ @SuppressLint("InternalInsetResource")
1937
+ int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
1938
+ if (resourceId > 0) {
1939
+ result = getContext().getResources().getDimensionPixelSize(resourceId);
1940
+ }
1941
+ return result;
1942
+ }
2396
1943
  }