@capgo/camera-preview 7.3.12 → 7.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CapgoCameraPreview.podspec +16 -13
  2. package/README.md +492 -73
  3. package/android/build.gradle +11 -0
  4. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  5. package/android/src/main/AndroidManifest.xml +5 -3
  6. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +968 -505
  7. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +3017 -0
  8. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +119 -0
  9. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +63 -0
  10. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +79 -0
  11. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +167 -0
  12. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +40 -0
  13. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +35 -0
  14. package/dist/docs.json +1041 -161
  15. package/dist/esm/definitions.d.ts +484 -84
  16. package/dist/esm/definitions.js +10 -1
  17. package/dist/esm/definitions.js.map +1 -1
  18. package/dist/esm/web.d.ts +78 -3
  19. package/dist/esm/web.js +813 -68
  20. package/dist/esm/web.js.map +1 -1
  21. package/dist/plugin.cjs.js +819 -68
  22. package/dist/plugin.cjs.js.map +1 -1
  23. package/dist/plugin.js +819 -68
  24. package/dist/plugin.js.map +1 -1
  25. package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +1658 -0
  26. package/ios/Sources/CapgoCameraPreviewPlugin/GridOverlayView.swift +65 -0
  27. package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +1548 -0
  28. package/ios/Tests/CameraPreviewPluginTests/CameraPreviewPluginTests.swift +15 -0
  29. package/package.json +2 -2
  30. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java +0 -1279
  31. package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java +0 -29
  32. package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java +0 -39
  33. package/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java +0 -461
  34. package/android/src/main/java/com/ahm/capacitor/camera/preview/TapGestureDetector.java +0 -24
  35. package/ios/Plugin/CameraController.swift +0 -809
  36. package/ios/Plugin/Info.plist +0 -24
  37. package/ios/Plugin/Plugin.h +0 -10
  38. package/ios/Plugin/Plugin.m +0 -18
  39. package/ios/Plugin/Plugin.swift +0 -511
  40. package/ios/Plugin.xcodeproj/project.pbxproj +0 -593
  41. package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  42. package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
  43. package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  44. package/ios/PluginTests/Info.plist +0 -22
  45. package/ios/PluginTests/PluginTests.swift +0 -83
  46. package/ios/Podfile +0 -13
  47. package/ios/Podfile.lock +0 -23
@@ -3,27 +3,19 @@ package com.ahm.capacitor.camera.preview;
3
3
  import static android.Manifest.permission.CAMERA;
4
4
  import static android.Manifest.permission.RECORD_AUDIO;
5
5
 
6
- import android.app.FragmentManager;
7
- import android.app.FragmentTransaction;
8
- import android.content.Context;
6
+ import android.Manifest;
9
7
  import android.content.pm.ActivityInfo;
10
- import android.graphics.Color;
11
- import android.graphics.ImageFormat;
12
- import android.graphics.Point;
13
- import android.hardware.Camera;
14
- import android.hardware.camera2.CameraAccessException;
15
- import android.hardware.camera2.CameraCharacteristics;
16
- import android.hardware.camera2.CameraManager;
17
- import android.hardware.camera2.params.StreamConfigurationMap;
8
+ import android.location.Location;
18
9
  import android.util.DisplayMetrics;
10
+ import android.util.Log;
19
11
  import android.util.Size;
20
- import android.util.TypedValue;
21
- import android.view.Display;
22
- import android.view.MotionEvent;
23
12
  import android.view.View;
24
13
  import android.view.ViewGroup;
25
- import android.widget.FrameLayout;
26
- import androidx.annotation.NonNull;
14
+ import android.webkit.WebView;
15
+ import com.ahm.capacitor.camera.preview.model.CameraDevice;
16
+ import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
17
+ import com.ahm.capacitor.camera.preview.model.LensInfo;
18
+ import com.ahm.capacitor.camera.preview.model.ZoomFactors;
27
19
  import com.getcapacitor.JSArray;
28
20
  import com.getcapacitor.JSObject;
29
21
  import com.getcapacitor.Logger;
@@ -34,11 +26,11 @@ import com.getcapacitor.PluginMethod;
34
26
  import com.getcapacitor.annotation.CapacitorPlugin;
35
27
  import com.getcapacitor.annotation.Permission;
36
28
  import com.getcapacitor.annotation.PermissionCallback;
37
- import java.io.File;
38
- import java.util.ArrayList;
29
+ import com.google.android.gms.location.FusedLocationProviderClient;
30
+ import com.google.android.gms.location.LocationServices;
39
31
  import java.util.List;
40
32
  import java.util.Objects;
41
- import org.json.JSONArray;
33
+ import org.json.JSONObject;
42
34
 
43
35
  @CapacitorPlugin(
44
36
  name = "CameraPreview",
@@ -51,33 +43,38 @@ import org.json.JSONArray;
51
43
  strings = { CAMERA },
52
44
  alias = CameraPreview.CAMERA_ONLY_PERMISSION_ALIAS
53
45
  ),
46
+ @Permission(
47
+ strings = {
48
+ Manifest.permission.ACCESS_COARSE_LOCATION,
49
+ Manifest.permission.ACCESS_FINE_LOCATION,
50
+ },
51
+ alias = CameraPreview.CAMERA_WITH_LOCATION_PERMISSION_ALIAS
52
+ ),
54
53
  }
55
54
  )
56
55
  public class CameraPreview
57
56
  extends Plugin
58
- implements CameraActivity.CameraPreviewListener {
57
+ implements CameraXView.CameraXViewListener {
59
58
 
60
59
  static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
61
60
  static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
62
-
63
- private static String VIDEO_FILE_PATH = "";
64
- private static final String VIDEO_FILE_EXTENSION = ".mp4";
61
+ static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS =
62
+ "cameraWithLocation";
65
63
 
66
64
  private String captureCallbackId = "";
67
65
  private String snapshotCallbackId = "";
68
- private String recordCallbackId = "";
69
66
  private String cameraStartCallbackId = "";
70
-
71
- // keep track of previously specified orientation to support locking orientation:
72
67
  private int previousOrientationRequest =
73
68
  ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
74
-
75
- private CameraActivity fragment;
76
- private final int containerViewId = 20;
69
+ private CameraXView cameraXView;
70
+ private FusedLocationProviderClient fusedLocationClient;
71
+ private Location lastLocation;
77
72
 
78
73
  @PluginMethod
79
74
  public void start(PluginCall call) {
80
- boolean disableAudio = call.getBoolean("disableAudio", false);
75
+ boolean disableAudio = Boolean.TRUE.equals(
76
+ call.getBoolean("disableAudio", true)
77
+ );
81
78
  String permissionAlias = disableAudio
82
79
  ? CAMERA_ONLY_PERMISSION_ALIAS
83
80
  : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
@@ -95,309 +92,349 @@ public class CameraPreview
95
92
 
96
93
  @PluginMethod
97
94
  public void flip(PluginCall call) {
98
- try {
99
- fragment.switchCamera();
100
- call.resolve();
101
- } catch (Exception e) {
102
- Logger.debug(getLogTag(), "Camera flip exception: " + e);
103
- call.reject("failed to flip camera");
95
+ if (cameraXView == null || !cameraXView.isRunning()) {
96
+ call.reject("Camera is not running");
97
+ return;
104
98
  }
99
+ cameraXView.flipCamera();
100
+ call.resolve();
105
101
  }
106
102
 
107
103
  @PluginMethod
108
- public void setOpacity(PluginCall call) {
109
- if (!this.hasCamera(call)) {
104
+ public void capture(final PluginCall call) {
105
+ if (cameraXView == null || !cameraXView.isRunning()) {
110
106
  call.reject("Camera is not running");
111
107
  return;
112
108
  }
113
109
 
114
- bridge.saveCall(call);
115
- Float opacity = Objects.requireNonNull(call.getFloat("opacity", 1F));
116
- fragment.setOpacity(opacity);
110
+ final boolean withExifLocation = call.getBoolean("withExifLocation", false);
111
+
112
+ if (withExifLocation) {
113
+ if (
114
+ getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) !=
115
+ PermissionState.GRANTED
116
+ ) {
117
+ requestPermissionForAlias(
118
+ CAMERA_WITH_LOCATION_PERMISSION_ALIAS,
119
+ call,
120
+ "captureWithLocationPermission"
121
+ );
122
+ } else {
123
+ getLocationAndCapture(call);
124
+ }
125
+ } else {
126
+ captureWithoutLocation(call);
127
+ }
117
128
  }
118
129
 
119
- @PluginMethod
120
- public void capture(PluginCall call) {
121
- if (!this.hasCamera(call)) {
122
- call.reject("Camera is not running");
123
- return;
130
+ @PermissionCallback
131
+ private void captureWithLocationPermission(PluginCall call) {
132
+ if (
133
+ getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) ==
134
+ PermissionState.GRANTED
135
+ ) {
136
+ getLocationAndCapture(call);
137
+ } else {
138
+ Logger.warn(
139
+ "Location permission denied. Capturing photo without location data."
140
+ );
141
+ captureWithoutLocation(call);
142
+ }
143
+ }
144
+
145
+ private void getLocationAndCapture(PluginCall call) {
146
+ if (fusedLocationClient == null) {
147
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(
148
+ getContext()
149
+ );
124
150
  }
151
+ fusedLocationClient
152
+ .getLastLocation()
153
+ .addOnSuccessListener(getActivity(), location -> {
154
+ lastLocation = location;
155
+ proceedWithCapture(call, lastLocation);
156
+ })
157
+ .addOnFailureListener(e -> {
158
+ Logger.error("Failed to get location: " + e.getMessage());
159
+ proceedWithCapture(call, null);
160
+ });
161
+ }
162
+
163
+ private void captureWithoutLocation(PluginCall call) {
164
+ proceedWithCapture(call, null);
165
+ }
166
+
167
+ private void proceedWithCapture(PluginCall call, Location location) {
125
168
  bridge.saveCall(call);
126
169
  captureCallbackId = call.getCallbackId();
127
170
 
128
171
  Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
129
- // Image Dimensions - Optional
130
- Integer width = Objects.requireNonNull(call.getInt("width", 0));
131
- Integer height = Objects.requireNonNull(call.getInt("height", 0));
132
- fragment.takePicture(width, height, quality);
172
+ final boolean saveToGallery = call.getBoolean("saveToGallery", false);
173
+ Integer width = call.getInt("width");
174
+ Integer height = call.getInt("height");
175
+ String aspectRatio = call.getString("aspectRatio");
176
+
177
+ cameraXView.capturePhoto(
178
+ quality,
179
+ saveToGallery,
180
+ width,
181
+ height,
182
+ aspectRatio,
183
+ location
184
+ );
133
185
  }
134
186
 
135
187
  @PluginMethod
136
188
  public void captureSample(PluginCall call) {
137
- if (!this.hasCamera(call)) {
189
+ if (cameraXView == null || !cameraXView.isRunning()) {
138
190
  call.reject("Camera is not running");
139
191
  return;
140
192
  }
141
193
  bridge.saveCall(call);
142
194
  snapshotCallbackId = call.getCallbackId();
143
-
144
195
  Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
145
- fragment.takeSnapshot(quality);
146
- }
147
-
148
- @PluginMethod
149
- public void getSupportedPictureSizes(final PluginCall call) {
150
- CameraManager cameraManager = (CameraManager) this.getContext()
151
- .getSystemService(Context.CAMERA_SERVICE);
152
-
153
- JSArray ret = new JSArray();
154
- try {
155
- String[] cameraIdList = cameraManager.getCameraIdList();
156
- for (String cameraId : cameraIdList) {
157
- CameraCharacteristics characteristics =
158
- cameraManager.getCameraCharacteristics(cameraId);
159
-
160
- // Determine the facing of the camera
161
- Integer lensFacing = characteristics.get(
162
- CameraCharacteristics.LENS_FACING
163
- );
164
- String facing = "Unknown";
165
- if (lensFacing != null) {
166
- switch (lensFacing) {
167
- case CameraCharacteristics.LENS_FACING_FRONT:
168
- facing = "Front";
169
- break;
170
- case CameraCharacteristics.LENS_FACING_BACK:
171
- facing = "Back";
172
- break;
173
- case CameraCharacteristics.LENS_FACING_EXTERNAL:
174
- facing = "External";
175
- break;
176
- }
177
- }
178
-
179
- StreamConfigurationMap map = characteristics.get(
180
- CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
181
- );
182
- if (map == null) {
183
- continue;
184
- }
185
-
186
- Size[] jpegSizes = map.getOutputSizes(ImageFormat.JPEG);
187
- JSObject camera = new JSObject();
188
- camera.put("facing", facing);
189
- JSArray supportedPictureSizes = new JSArray();
190
- if (jpegSizes != null) {
191
- for (Size size : jpegSizes) {
192
- JSObject sizeJson = new JSObject();
193
- sizeJson.put("width", size.getWidth());
194
- sizeJson.put("height", size.getHeight());
195
- supportedPictureSizes.put(sizeJson);
196
- }
197
- camera.put("supportedPictureSizes", supportedPictureSizes);
198
- ret.put(camera);
199
- }
200
- }
201
- JSObject finalRet = new JSObject();
202
- finalRet.put("supportedPictureSizes", ret);
203
- call.resolve(finalRet);
204
- } catch (CameraAccessException ex) {
205
- Logger.error(getLogTag(), "Cannot call getSupportedPictureSizes", ex);
206
- call.reject(
207
- String.format("Cannot call getSupportedPictureSizes. Error: %s", ex)
208
- );
209
- }
196
+ cameraXView.captureSample(quality);
210
197
  }
211
198
 
212
199
  @PluginMethod
213
200
  public void stop(final PluginCall call) {
214
201
  bridge
215
202
  .getActivity()
216
- .runOnUiThread(
217
- new Runnable() {
218
- @Override
219
- public void run() {
220
- FrameLayout containerView = getBridge()
221
- .getActivity()
222
- .findViewById(containerViewId);
223
-
224
- // allow orientation changes after closing camera:
225
- getBridge()
226
- .getActivity()
227
- .setRequestedOrientation(previousOrientationRequest);
228
-
229
- if (containerView != null) {
230
- ((ViewGroup) getBridge().getWebView().getParent()).removeView(
231
- containerView
232
- );
233
- getBridge().getWebView().setBackgroundColor(Color.WHITE);
234
- FragmentManager fragmentManager = getActivity()
235
- .getFragmentManager();
236
- FragmentTransaction fragmentTransaction =
237
- fragmentManager.beginTransaction();
238
- fragmentTransaction.remove(fragment);
239
- fragmentTransaction.commit();
240
- fragment = null;
241
-
242
- call.resolve();
243
- } else {
244
- call.reject("camera already stopped");
245
- }
246
- }
203
+ .runOnUiThread(() -> {
204
+ getBridge()
205
+ .getActivity()
206
+ .setRequestedOrientation(previousOrientationRequest);
207
+
208
+ if (cameraXView != null && cameraXView.isRunning()) {
209
+ cameraXView.stopSession();
210
+ cameraXView = null;
247
211
  }
248
- );
212
+ call.resolve();
213
+ });
249
214
  }
250
215
 
251
216
  @PluginMethod
252
217
  public void getSupportedFlashModes(PluginCall call) {
253
- if (!this.hasCamera(call)) {
254
- call.reject("Camera is not running");
255
- return;
218
+ List<String> supportedFlashModes = cameraXView.getSupportedFlashModes();
219
+ JSArray jsonFlashModes = new JSArray();
220
+ for (String mode : supportedFlashModes) {
221
+ jsonFlashModes.put(mode);
256
222
  }
257
-
258
- Camera camera = fragment.getCamera();
259
- Camera.Parameters params = camera.getParameters();
260
- List<String> supportedFlashModes;
261
- supportedFlashModes = params.getSupportedFlashModes();
262
- JSONArray jsonFlashModes = new JSONArray();
263
-
264
- if (supportedFlashModes != null) {
265
- for (int i = 0; i < supportedFlashModes.size(); i++) {
266
- jsonFlashModes.put(supportedFlashModes.get(i));
267
- }
268
- }
269
-
270
223
  JSObject jsObject = new JSObject();
271
224
  jsObject.put("result", jsonFlashModes);
272
225
  call.resolve(jsObject);
273
226
  }
274
227
 
275
228
  @PluginMethod
276
- public void getHorizontalFov(PluginCall call) {
277
- if (!this.hasCamera(call)) {
278
- call.reject("Camera is not running");
229
+ public void setFlashMode(PluginCall call) {
230
+ String flashMode = call.getString("flashMode");
231
+ if (flashMode == null || flashMode.isEmpty()) {
232
+ call.reject("flashMode required parameter is missing");
279
233
  return;
280
234
  }
235
+ cameraXView.setFlashMode(flashMode);
236
+ call.resolve();
237
+ }
281
238
 
282
- Camera camera = fragment.getCamera();
283
- Camera.Parameters params = camera.getParameters();
284
-
285
- float horizontalViewAngle = params.getHorizontalViewAngle();
239
+ @PluginMethod
240
+ public void getAvailableDevices(PluginCall call) {
241
+ List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
242
+ getContext()
243
+ );
244
+ JSArray devicesArray = new JSArray();
245
+ for (CameraDevice device : devices) {
246
+ JSObject deviceJson = new JSObject();
247
+ deviceJson.put("deviceId", device.getDeviceId());
248
+ deviceJson.put("label", device.getLabel());
249
+ deviceJson.put("position", device.getPosition());
250
+ JSArray lensesArray = new JSArray();
251
+ for (com.ahm.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
252
+ JSObject lensJson = new JSObject();
253
+ lensJson.put("focalLength", lens.getFocalLength());
254
+ lensJson.put("deviceType", lens.getDeviceType());
255
+ lensJson.put("baseZoomRatio", lens.getBaseZoomRatio());
256
+ lensJson.put("digitalZoom", lens.getDigitalZoom());
257
+ lensesArray.put(lensJson);
258
+ }
259
+ deviceJson.put("lenses", lensesArray);
260
+ deviceJson.put("minZoom", device.getMinZoom());
261
+ deviceJson.put("maxZoom", device.getMaxZoom());
262
+ devicesArray.put(deviceJson);
263
+ }
264
+ JSObject result = new JSObject();
265
+ result.put("devices", devicesArray);
266
+ call.resolve(result);
267
+ }
286
268
 
287
- JSObject jsObject = new JSObject();
288
- jsObject.put("result", horizontalViewAngle);
289
- call.resolve(jsObject);
269
+ @PluginMethod
270
+ public void getZoom(PluginCall call) {
271
+ ZoomFactors zoomFactors = cameraXView.getZoomFactors();
272
+ JSObject result = new JSObject();
273
+ result.put("min", zoomFactors.getMin());
274
+ result.put("max", zoomFactors.getMax());
275
+ result.put("current", zoomFactors.getCurrent());
276
+ call.resolve(result);
290
277
  }
291
278
 
292
279
  @PluginMethod
293
- public void setFlashMode(PluginCall call) {
294
- if (!this.hasCamera(call)) {
280
+ public void setZoom(PluginCall call) {
281
+ if (cameraXView == null || !cameraXView.isRunning()) {
295
282
  call.reject("Camera is not running");
296
283
  return;
297
284
  }
298
-
299
- String flashMode = call.getString("flashMode");
300
- if (flashMode == null || flashMode.isEmpty()) {
301
- call.reject("flashMode required parameter is missing");
285
+ Float level = call.getFloat("level");
286
+ if (level == null) {
287
+ call.reject("level parameter is required");
302
288
  return;
303
289
  }
290
+ Boolean autoFocus = call.getBoolean("autoFocus", true);
291
+ try {
292
+ cameraXView.setZoom(level, autoFocus);
293
+ call.resolve();
294
+ } catch (Exception e) {
295
+ call.reject("Failed to set zoom: " + e.getMessage());
296
+ }
297
+ }
304
298
 
305
- Camera camera = fragment.getCamera();
306
- Camera.Parameters params = camera.getParameters();
307
-
308
- List<String> supportedFlashModes;
309
- supportedFlashModes = camera.getParameters().getSupportedFlashModes();
310
- if (
311
- supportedFlashModes != null && supportedFlashModes.contains(flashMode)
312
- ) {
313
- params.setFlashMode(flashMode);
314
- } else {
315
- call.reject("Flash mode not recognised: " + flashMode);
299
+ @PluginMethod
300
+ public void setFocus(PluginCall call) {
301
+ if (cameraXView == null || !cameraXView.isRunning()) {
302
+ call.reject("Camera is not running");
303
+ return;
304
+ }
305
+ Float x = call.getFloat("x");
306
+ Float y = call.getFloat("y");
307
+ if (x == null || y == null) {
308
+ call.reject("x and y parameters are required");
309
+ return;
310
+ }
311
+ // Reject if values are outside 0-1 range
312
+ if (x < 0f || x > 1f || y < 0f || y > 1f) {
313
+ call.reject("Focus coordinates must be between 0 and 1");
316
314
  return;
317
315
  }
318
316
 
319
- fragment.setCameraParameters(params);
320
-
321
- call.resolve();
317
+ getActivity()
318
+ .runOnUiThread(() -> {
319
+ try {
320
+ cameraXView.setFocus(x, y);
321
+ call.resolve();
322
+ } catch (Exception e) {
323
+ call.reject("Failed to set focus: " + e.getMessage());
324
+ }
325
+ });
322
326
  }
323
327
 
324
328
  @PluginMethod
325
- public void startRecordVideo(final PluginCall call) {
326
- if (!this.hasCamera(call)) {
329
+ public void setDeviceId(PluginCall call) {
330
+ String deviceId = call.getString("deviceId");
331
+ if (deviceId == null || deviceId.isEmpty()) {
332
+ call.reject("deviceId parameter is required");
333
+ return;
334
+ }
335
+ if (cameraXView == null || !cameraXView.isRunning()) {
327
336
  call.reject("Camera is not running");
328
337
  return;
329
338
  }
330
- final String filename = "videoTmp";
331
- VIDEO_FILE_PATH = getActivity().getCacheDir().toString() + "/";
332
-
333
- final String position = Objects.requireNonNull(
334
- call.getString("position", "front")
335
- );
336
- final Integer width = Objects.requireNonNull(call.getInt("width", 0));
337
- final Integer height = Objects.requireNonNull(call.getInt("height", 0));
338
- final Boolean withFlash = Objects.requireNonNull(
339
- call.getBoolean("withFlash", false)
340
- );
341
- final Integer maxDuration = Objects.requireNonNull(
342
- call.getInt("maxDuration", 0)
343
- );
344
- // final Integer quality = Objects.requireNonNull(call.getInt("quality", 0));
345
- bridge.saveCall(call);
346
- recordCallbackId = call.getCallbackId();
339
+ cameraXView.switchToDevice(deviceId);
340
+ call.resolve();
341
+ }
347
342
 
348
- bridge
349
- .getActivity()
350
- .runOnUiThread(
351
- new Runnable() {
352
- @Override
353
- public void run() {
354
- fragment.startRecord(
355
- getFilePath(),
356
- position,
357
- width,
358
- height,
359
- 70,
360
- withFlash,
361
- maxDuration
362
- );
363
- }
364
- }
365
- );
343
+ @PluginMethod
344
+ public void getSupportedPictureSizes(final PluginCall call) {
345
+ JSArray supportedPictureSizesResult = new JSArray();
346
+ List<Size> rearSizes = CameraXView.getSupportedPictureSizes("rear");
347
+ JSObject rear = new JSObject();
348
+ rear.put("facing", "rear");
349
+ JSArray rearSizesJs = new JSArray();
350
+ for (Size size : rearSizes) {
351
+ JSObject sizeJs = new JSObject();
352
+ sizeJs.put("width", size.getWidth());
353
+ sizeJs.put("height", size.getHeight());
354
+ rearSizesJs.put(sizeJs);
355
+ }
356
+ rear.put("supportedPictureSizes", rearSizesJs);
357
+ supportedPictureSizesResult.put(rear);
358
+
359
+ List<Size> frontSizes = CameraXView.getSupportedPictureSizes("front");
360
+ JSObject front = new JSObject();
361
+ front.put("facing", "front");
362
+ JSArray frontSizesJs = new JSArray();
363
+ for (Size size : frontSizes) {
364
+ JSObject sizeJs = new JSObject();
365
+ sizeJs.put("width", size.getWidth());
366
+ sizeJs.put("height", size.getHeight());
367
+ frontSizesJs.put(sizeJs);
368
+ }
369
+ front.put("supportedPictureSizes", frontSizesJs);
370
+ supportedPictureSizesResult.put(front);
366
371
 
367
- call.resolve();
372
+ JSObject ret = new JSObject();
373
+ ret.put("supportedPictureSizes", supportedPictureSizesResult);
374
+ call.resolve(ret);
368
375
  }
369
376
 
370
377
  @PluginMethod
371
- public void stopRecordVideo(PluginCall call) {
372
- if (!this.hasCamera(call)) {
378
+ public void setOpacity(PluginCall call) {
379
+ if (cameraXView == null || !cameraXView.isRunning()) {
373
380
  call.reject("Camera is not running");
374
381
  return;
375
382
  }
383
+ Float opacity = call.getFloat("opacity", 1.0f);
384
+ cameraXView.setOpacity(opacity);
385
+ call.resolve();
386
+ }
376
387
 
377
- System.out.println("stopRecordVideo - Callbackid=" + call.getCallbackId());
388
+ @PluginMethod
389
+ public void getHorizontalFov(PluginCall call) {
390
+ // CameraX does not provide a simple way to get FoV.
391
+ // This would require Camera2 interop to access camera characteristics.
392
+ // Returning a default/estimated value.
393
+ JSObject ret = new JSObject();
394
+ ret.put("result", 60.0); // A common default FoV
395
+ call.resolve(ret);
396
+ }
378
397
 
379
- bridge.saveCall(call);
380
- recordCallbackId = call.getCallbackId();
398
+ @PluginMethod
399
+ public void getDeviceId(PluginCall call) {
400
+ if (cameraXView == null || !cameraXView.isRunning()) {
401
+ call.reject("Camera is not running");
402
+ return;
403
+ }
404
+ JSObject ret = new JSObject();
405
+ ret.put("deviceId", cameraXView.getCurrentDeviceId());
406
+ call.resolve(ret);
407
+ }
381
408
 
382
- // bridge.getActivity().runOnUiThread(new Runnable() {
383
- // @Override
384
- // public void run() {
385
- // fragment.stopRecord();
386
- // }
387
- // });
409
+ @PluginMethod
410
+ public void getFlashMode(PluginCall call) {
411
+ if (cameraXView == null || !cameraXView.isRunning()) {
412
+ call.reject("Camera is not running");
413
+ return;
414
+ }
415
+ JSObject ret = new JSObject();
416
+ ret.put("flashMode", cameraXView.getFlashMode());
417
+ call.resolve(ret);
418
+ }
388
419
 
389
- fragment.stopRecord();
390
- // call.resolve();
420
+ @PluginMethod
421
+ public void isRunning(PluginCall call) {
422
+ boolean running = cameraXView != null && cameraXView.isRunning();
423
+ JSObject jsObject = new JSObject();
424
+ jsObject.put("isRunning", running);
425
+ call.resolve(jsObject);
391
426
  }
392
427
 
393
428
  @PermissionCallback
394
429
  private void handleCameraPermissionResult(PluginCall call) {
395
- boolean disableAudio = call.getBoolean("disableAudio", false);
396
- String permissionAlias = disableAudio
397
- ? CAMERA_ONLY_PERMISSION_ALIAS
398
- : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
399
-
400
- if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
430
+ if (
431
+ PermissionState.GRANTED.equals(
432
+ getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
433
+ ) ||
434
+ PermissionState.GRANTED.equals(
435
+ getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS)
436
+ )
437
+ ) {
401
438
  startCamera(call);
402
439
  } else {
403
440
  call.reject("Permission failed");
@@ -405,327 +442,753 @@ public class CameraPreview
405
442
  }
406
443
 
407
444
  private void startCamera(final PluginCall call) {
408
- String position = call.getString("position");
409
-
410
- if (position == null || position.isEmpty() || "rear".equals(position)) {
411
- position = "back";
412
- } else {
413
- position = "front";
414
- }
415
-
416
- @NonNull
417
- final Integer x = Objects.requireNonNull(call.getInt("x", 0));
418
- @NonNull
419
- final Integer y = Objects.requireNonNull(call.getInt("y", 0));
420
- @NonNull
421
- final Integer width = Objects.requireNonNull(call.getInt("width", 0));
422
- @NonNull
423
- final Integer height = Objects.requireNonNull(call.getInt("height", 0));
424
- @NonNull
425
- final Integer paddingBottom = Objects.requireNonNull(
426
- call.getInt("paddingBottom", 0)
445
+ String positionParam = call.getString("position");
446
+ String originalDeviceId = call.getString("deviceId");
447
+ String deviceId = originalDeviceId; // Use a mutable variable
448
+
449
+ final String position = (positionParam == null ||
450
+ positionParam.isEmpty() ||
451
+ "rear".equals(positionParam) ||
452
+ "back".equals(positionParam))
453
+ ? "back"
454
+ : "front";
455
+ // Use -1 as default to indicate centering is needed when x/y not provided
456
+ final Integer xParam = call.getInt("x");
457
+ final Integer yParam = call.getInt("y");
458
+ final int x = xParam != null ? xParam : -1;
459
+ final int y = yParam != null ? yParam : -1;
460
+
461
+ Log.d("CameraPreview", "========================");
462
+ Log.d("CameraPreview", "CAMERA POSITION TRACKING START:");
463
+ Log.d(
464
+ "CameraPreview",
465
+ "1. RAW PARAMS - xParam: " + xParam + ", yParam: " + yParam
427
466
  );
428
- final Boolean toBack = Objects.requireNonNull(
429
- call.getBoolean("toBack", false)
467
+ Log.d(
468
+ "CameraPreview",
469
+ "2. AFTER DEFAULT - x: " +
470
+ x +
471
+ " (center=" +
472
+ (x == -1) +
473
+ "), y: " +
474
+ y +
475
+ " (center=" +
476
+ (y == -1) +
477
+ ")"
430
478
  );
431
- final Boolean storeToFile = Objects.requireNonNull(
479
+ final int width = call.getInt("width", 0);
480
+ final int height = call.getInt("height", 0);
481
+ final int paddingBottom = call.getInt("paddingBottom", 0);
482
+ final boolean toBack = Boolean.TRUE.equals(call.getBoolean("toBack", true));
483
+ final boolean storeToFile = Boolean.TRUE.equals(
432
484
  call.getBoolean("storeToFile", false)
433
485
  );
434
- final Boolean enableOpacity = Objects.requireNonNull(
486
+ final boolean enableOpacity = Boolean.TRUE.equals(
435
487
  call.getBoolean("enableOpacity", false)
436
488
  );
437
- final Boolean enableZoom = Objects.requireNonNull(
489
+ final boolean enableZoom = Boolean.TRUE.equals(
438
490
  call.getBoolean("enableZoom", false)
439
491
  );
440
- final Boolean disableExifHeaderStripping = Objects.requireNonNull(
492
+ final boolean disableExifHeaderStripping = Boolean.TRUE.equals(
441
493
  call.getBoolean("disableExifHeaderStripping", false)
442
494
  );
443
- final Boolean lockOrientation = Objects.requireNonNull(
495
+ final boolean lockOrientation = Boolean.TRUE.equals(
444
496
  call.getBoolean("lockAndroidOrientation", false)
445
497
  );
498
+ final boolean disableAudio = Boolean.TRUE.equals(
499
+ call.getBoolean("disableAudio", true)
500
+ );
501
+ final String aspectRatio = call.getString("aspectRatio", "4:3");
502
+ final String gridMode = call.getString("gridMode", "none");
503
+ final String positioning = call.getString("positioning", "top");
504
+ final float initialZoomLevel = call.getFloat("initialZoomLevel", 1.0f);
505
+
506
+ // Check for conflict between aspectRatio and size
507
+ if (
508
+ call.getData().has("aspectRatio") &&
509
+ (call.getData().has("width") || call.getData().has("height"))
510
+ ) {
511
+ call.reject(
512
+ "Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."
513
+ );
514
+ return;
515
+ }
516
+
517
+ float targetZoom = initialZoomLevel;
518
+ // Check if the selected device is a physical ultra-wide
519
+ if (originalDeviceId != null) {
520
+ List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
521
+ getContext()
522
+ );
523
+ for (CameraDevice device : devices) {
524
+ if (
525
+ originalDeviceId.equals(device.getDeviceId()) && !device.isLogical()
526
+ ) {
527
+ for (LensInfo lens : device.getLenses()) {
528
+ if ("ultraWide".equals(lens.getDeviceType())) {
529
+ Log.d(
530
+ "CameraPreview",
531
+ "Ultra-wide lens selected. Targeting 0.5x zoom on logical camera."
532
+ );
533
+ targetZoom = 0.5f;
534
+ // Force the use of the logical camera by clearing the specific deviceId
535
+ deviceId = null;
536
+ break;
537
+ }
538
+ }
539
+ }
540
+ if (deviceId == null) break; // Exit outer loop once we've made our decision
541
+ }
542
+ }
543
+
446
544
  previousOrientationRequest = getBridge()
447
545
  .getActivity()
448
546
  .getRequestedOrientation();
547
+ cameraXView = new CameraXView(getContext(), getBridge().getWebView());
548
+ cameraXView.setListener(this);
449
549
 
450
- fragment = new CameraActivity();
451
- fragment.setEventListener(this);
452
- fragment.defaultCamera = position;
453
- fragment.tapToTakePicture = false;
454
- fragment.dragEnabled = false;
455
- fragment.tapToFocus = true;
456
- fragment.disableExifHeaderStripping = disableExifHeaderStripping;
457
- fragment.storeToFile = storeToFile;
458
- fragment.toBack = toBack;
459
- fragment.enableOpacity = enableOpacity;
460
- fragment.enableZoom = enableZoom;
461
- fragment.disableAudio = call.getBoolean("disableAudio", false);
462
-
463
- bridge
550
+ String finalDeviceId = deviceId;
551
+ float finalTargetZoom = targetZoom;
552
+ getBridge()
464
553
  .getActivity()
465
- .runOnUiThread(
466
- new Runnable() {
467
- @Override
468
- public void run() {
469
- DisplayMetrics metrics = getBridge()
470
- .getActivity()
471
- .getResources()
472
- .getDisplayMetrics();
473
- // lock orientation if specified in options:
474
- if (lockOrientation) {
475
- getBridge()
476
- .getActivity()
477
- .setRequestedOrientation(
478
- ActivityInfo.SCREEN_ORIENTATION_LOCKED
479
- );
480
- }
554
+ .runOnUiThread(() -> {
555
+ DisplayMetrics metrics = getBridge()
556
+ .getActivity()
557
+ .getResources()
558
+ .getDisplayMetrics();
559
+ if (lockOrientation) {
560
+ getBridge()
561
+ .getActivity()
562
+ .setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
563
+ }
481
564
 
482
- // offset
483
- int computedX = (int) TypedValue.applyDimension(
484
- TypedValue.COMPLEX_UNIT_DIP,
485
- x,
486
- metrics
487
- );
488
- int computedY = (int) TypedValue.applyDimension(
489
- TypedValue.COMPLEX_UNIT_DIP,
490
- y,
491
- metrics
492
- );
565
+ // Debug: Let's check all the positioning information
566
+ ViewGroup webViewParent = (ViewGroup) getBridge()
567
+ .getWebView()
568
+ .getParent();
569
+
570
+ // Get webview position in different coordinate systems
571
+ int[] webViewLocationInWindow = new int[2];
572
+ int[] webViewLocationOnScreen = new int[2];
573
+ getBridge().getWebView().getLocationInWindow(webViewLocationInWindow);
574
+ getBridge().getWebView().getLocationOnScreen(webViewLocationOnScreen);
575
+
576
+ int webViewLeft = getBridge().getWebView().getLeft();
577
+ int webViewTop = getBridge().getWebView().getTop();
578
+
579
+ // Check parent position too
580
+ int[] parentLocationInWindow = new int[2];
581
+ int[] parentLocationOnScreen = new int[2];
582
+ webViewParent.getLocationInWindow(parentLocationInWindow);
583
+ webViewParent.getLocationOnScreen(parentLocationOnScreen);
584
+
585
+ // Calculate pixel ratio
586
+ float pixelRatio = metrics.density;
587
+
588
+ // The key insight: JavaScript coordinates are relative to the WebView's viewport
589
+ // If the WebView is positioned below the status bar (webViewLocationOnScreen[1] > 0),
590
+ // we need to add that offset when placing native views
591
+ int webViewTopInset = webViewLocationOnScreen[1];
592
+ boolean isEdgeToEdgeActive = webViewLocationOnScreen[1] > 0;
593
+
594
+ // Log all the positioning information for debugging
595
+ Log.d("CameraPreview", "WebView Position Debug:");
596
+ Log.d("CameraPreview", " - webView.getTop(): " + webViewTop);
597
+ Log.d("CameraPreview", " - webView.getLeft(): " + webViewLeft);
598
+ Log.d(
599
+ "CameraPreview",
600
+ " - webView locationInWindow: (" +
601
+ webViewLocationInWindow[0] +
602
+ ", " +
603
+ webViewLocationInWindow[1] +
604
+ ")"
605
+ );
606
+ Log.d(
607
+ "CameraPreview",
608
+ " - webView locationOnScreen: (" +
609
+ webViewLocationOnScreen[0] +
610
+ ", " +
611
+ webViewLocationOnScreen[1] +
612
+ ")"
613
+ );
614
+ Log.d(
615
+ "CameraPreview",
616
+ " - parent locationInWindow: (" +
617
+ parentLocationInWindow[0] +
618
+ ", " +
619
+ parentLocationInWindow[1] +
620
+ ")"
621
+ );
622
+ Log.d(
623
+ "CameraPreview",
624
+ " - parent locationOnScreen: (" +
625
+ parentLocationOnScreen[0] +
626
+ ", " +
627
+ parentLocationOnScreen[1] +
628
+ ")"
629
+ );
493
630
 
494
- // size
495
- int computedWidth;
496
- int computedHeight;
497
- int computedPaddingBottom;
631
+ // Check if WebView has margins
632
+ View webView = getBridge().getWebView();
633
+ ViewGroup.LayoutParams webViewLayoutParams = webView.getLayoutParams();
634
+ if (webViewLayoutParams instanceof ViewGroup.MarginLayoutParams) {
635
+ ViewGroup.MarginLayoutParams marginParams =
636
+ (ViewGroup.MarginLayoutParams) webViewLayoutParams;
637
+ Log.d(
638
+ "CameraPreview",
639
+ " - webView margins: left=" +
640
+ marginParams.leftMargin +
641
+ ", top=" +
642
+ marginParams.topMargin +
643
+ ", right=" +
644
+ marginParams.rightMargin +
645
+ ", bottom=" +
646
+ marginParams.bottomMargin
647
+ );
648
+ }
498
649
 
499
- if (paddingBottom != 0) {
500
- computedPaddingBottom = (int) TypedValue.applyDimension(
501
- TypedValue.COMPLEX_UNIT_DIP,
502
- paddingBottom,
503
- metrics
504
- );
505
- } else {
506
- computedPaddingBottom = 0;
507
- }
650
+ // Check WebView padding
651
+ Log.d(
652
+ "CameraPreview",
653
+ " - webView padding: left=" +
654
+ webView.getPaddingLeft() +
655
+ ", top=" +
656
+ webView.getPaddingTop() +
657
+ ", right=" +
658
+ webView.getPaddingRight() +
659
+ ", bottom=" +
660
+ webView.getPaddingBottom()
661
+ );
508
662
 
509
- if (width != 0) {
510
- computedWidth = (int) TypedValue.applyDimension(
511
- TypedValue.COMPLEX_UNIT_DIP,
512
- width,
513
- metrics
514
- );
515
- } else {
516
- Display defaultDisplay = getBridge()
517
- .getActivity()
518
- .getWindowManager()
519
- .getDefaultDisplay();
520
- final Point size = new Point();
521
- defaultDisplay.getSize(size);
522
-
523
- computedWidth = (int) TypedValue.applyDimension(
524
- TypedValue.COMPLEX_UNIT_PX,
525
- size.x,
526
- metrics
527
- );
528
- }
663
+ Log.d("CameraPreview", " - Using webViewTopInset: " + webViewTopInset);
664
+ Log.d("CameraPreview", " - isEdgeToEdgeActive: " + isEdgeToEdgeActive);
665
+
666
+ // Calculate position - center if x or y is -1
667
+ int computedX;
668
+ int computedY;
669
+
670
+ // Calculate dimensions first
671
+ int computedWidth = width != 0
672
+ ? (int) (width * pixelRatio)
673
+ : getBridge().getWebView().getWidth();
674
+ int computedHeight = height != 0
675
+ ? (int) (height * pixelRatio)
676
+ : getBridge().getWebView().getHeight();
677
+ computedHeight -= (int) (paddingBottom * pixelRatio);
678
+
679
+ Log.d("CameraPreview", "========================");
680
+ Log.d("CameraPreview", "POSITIONING CALCULATIONS:");
681
+ Log.d(
682
+ "CameraPreview",
683
+ "1. INPUT - x: " +
684
+ x +
685
+ ", y: " +
686
+ y +
687
+ ", width: " +
688
+ width +
689
+ ", height: " +
690
+ height
691
+ );
692
+ Log.d("CameraPreview", "2. PIXEL RATIO: " + pixelRatio);
693
+ Log.d(
694
+ "CameraPreview",
695
+ "3. SCREEN - width: " +
696
+ metrics.widthPixels +
697
+ ", height: " +
698
+ metrics.heightPixels
699
+ );
700
+ Log.d(
701
+ "CameraPreview",
702
+ "4. WEBVIEW - width: " +
703
+ getBridge().getWebView().getWidth() +
704
+ ", height: " +
705
+ getBridge().getWebView().getHeight()
706
+ );
707
+ Log.d(
708
+ "CameraPreview",
709
+ "5. COMPUTED DIMENSIONS - width: " +
710
+ computedWidth +
711
+ ", height: " +
712
+ computedHeight
713
+ );
529
714
 
530
- if (height != 0) {
531
- computedHeight =
532
- (int) TypedValue.applyDimension(
533
- TypedValue.COMPLEX_UNIT_DIP,
534
- height,
535
- metrics
536
- ) -
537
- computedPaddingBottom;
538
- } else {
539
- Display defaultDisplay = getBridge()
540
- .getActivity()
541
- .getWindowManager()
542
- .getDefaultDisplay();
543
- final Point size = new Point();
544
- defaultDisplay.getSize(size);
545
-
546
- computedHeight =
547
- (int) TypedValue.applyDimension(
548
- TypedValue.COMPLEX_UNIT_PX,
549
- size.y,
550
- metrics
551
- ) -
552
- computedPaddingBottom;
553
- }
715
+ if (x == -1) {
716
+ // Center horizontally
717
+ int screenWidth = metrics.widthPixels;
718
+ computedX = (screenWidth - computedWidth) / 2;
719
+ Log.d(
720
+ "CameraPreview",
721
+ "Centering horizontally: screenWidth=" +
722
+ screenWidth +
723
+ ", computedWidth=" +
724
+ computedWidth +
725
+ ", computedX=" +
726
+ computedX
727
+ );
728
+ } else {
729
+ computedX = (int) (x * pixelRatio);
730
+ Log.d(
731
+ "CameraPreview",
732
+ "Using provided X position: " +
733
+ x +
734
+ " * " +
735
+ pixelRatio +
736
+ " = " +
737
+ computedX
738
+ );
739
+ }
554
740
 
555
- fragment.setRect(
556
- computedX,
557
- computedY,
558
- computedWidth,
559
- computedHeight
560
- );
741
+ if (y == -1) {
742
+ // Position vertically based on positioning parameter
743
+ int screenHeight = metrics.heightPixels;
561
744
 
562
- FrameLayout containerView = getBridge()
563
- .getActivity()
564
- .findViewById(containerViewId);
565
- if (containerView == null) {
566
- containerView = new FrameLayout(
567
- getActivity().getApplicationContext()
745
+ switch (positioning) {
746
+ case "top":
747
+ computedY = 0;
748
+ Log.d("CameraPreview", "Positioning at top: computedY=0");
749
+ break;
750
+ case "bottom":
751
+ computedY = screenHeight - computedHeight;
752
+ Log.d(
753
+ "CameraPreview",
754
+ "Positioning at bottom: screenHeight=" +
755
+ screenHeight +
756
+ ", computedHeight=" +
757
+ computedHeight +
758
+ ", computedY=" +
759
+ computedY
568
760
  );
569
- containerView.setId(containerViewId);
570
-
571
- getBridge().getWebView().setBackgroundColor(Color.TRANSPARENT);
572
- ((ViewGroup) getBridge().getWebView().getParent()).addView(
573
- containerView
761
+ break;
762
+ case "center":
763
+ default:
764
+ // Center vertically
765
+ if (isEdgeToEdgeActive) {
766
+ // When WebView is offset from top, center within the available space
767
+ // The camera should be centered in the full screen, not just the WebView area
768
+ computedY = (screenHeight - computedHeight) / 2;
769
+ Log.d(
770
+ "CameraPreview",
771
+ "Centering vertically with WebView offset: screenHeight=" +
772
+ screenHeight +
773
+ ", webViewTop=" +
774
+ webViewTopInset +
775
+ ", computedHeight=" +
776
+ computedHeight +
777
+ ", computedY=" +
778
+ computedY
779
+ );
780
+ } else {
781
+ // Normal mode - use full screen height
782
+ computedY = (screenHeight - computedHeight) / 2;
783
+ Log.d(
784
+ "CameraPreview",
785
+ "Centering vertically (normal): screenHeight=" +
786
+ screenHeight +
787
+ ", computedHeight=" +
788
+ computedHeight +
789
+ ", computedY=" +
790
+ computedY
574
791
  );
575
- if (toBack) {
576
- getBridge()
577
- .getWebView()
578
- .getParent()
579
- .bringChildToFront(getBridge().getWebView());
580
- setupBroadcast();
581
792
  }
582
-
583
- FragmentManager fragmentManager = getBridge()
584
- .getActivity()
585
- .getFragmentManager();
586
- FragmentTransaction fragmentTransaction =
587
- fragmentManager.beginTransaction();
588
- fragmentTransaction.add(containerView.getId(), fragment);
589
- fragmentTransaction.commit();
590
-
591
- // NOTE: we don't return invoke call.resolve here because it must be invoked in onCameraStarted
592
- // otherwise the plugin start method might resolve/return before the camera is actually set in CameraActivity
593
- // onResume method (see this line mCamera = Camera.open(defaultCameraId);) and the next subsequent plugin
594
- // method invocations (for example, getSupportedFlashModes) might fails with "Camera is not running" error
595
- // because camera is not available yet and hasCamera method will return false
596
- // Please also see https://developer.android.com/reference/android/hardware/Camera.html#open%28int%29
597
- bridge.saveCall(call);
598
- cameraStartCallbackId = call.getCallbackId();
599
- } else {
600
- call.reject("camera already started");
601
- }
793
+ break;
794
+ }
795
+ } else {
796
+ computedY = (int) (y * pixelRatio);
797
+ // If edge-to-edge is active, JavaScript Y is relative to WebView content area
798
+ // We need to add the inset to get absolute screen position
799
+ if (isEdgeToEdgeActive) {
800
+ computedY += webViewTopInset;
801
+ Log.d(
802
+ "CameraPreview",
803
+ "Edge-to-edge adjustment: Y position " +
804
+ (int) (y * pixelRatio) +
805
+ " + inset " +
806
+ webViewTopInset +
807
+ " = " +
808
+ computedY
809
+ );
602
810
  }
811
+ Log.d(
812
+ "CameraPreview",
813
+ "Using provided Y position: " +
814
+ y +
815
+ " * " +
816
+ pixelRatio +
817
+ " = " +
818
+ computedY +
819
+ (isEdgeToEdgeActive ? " (adjusted for edge-to-edge)" : "")
820
+ );
603
821
  }
604
- );
605
- }
606
822
 
607
- @Override
608
- protected void handleOnResume() {
609
- super.handleOnResume();
823
+ Log.d(
824
+ "CameraPreview",
825
+ "2b. EDGE-TO-EDGE - " +
826
+ (isEdgeToEdgeActive
827
+ ? "ACTIVE (inset=" + webViewTopInset + ")"
828
+ : "INACTIVE")
829
+ );
830
+ Log.d(
831
+ "CameraPreview",
832
+ "3. COMPUTED POSITION - x=" + computedX + ", y=" + computedY
833
+ );
834
+ Log.d(
835
+ "CameraPreview",
836
+ "4. COMPUTED SIZE - width=" +
837
+ computedWidth +
838
+ ", height=" +
839
+ computedHeight
840
+ );
841
+ Log.d("CameraPreview", "=== COORDINATE DEBUG ===");
842
+ Log.d(
843
+ "CameraPreview",
844
+ "WebView getLeft/getTop: (" + webViewLeft + ", " + webViewTop + ")"
845
+ );
846
+ Log.d(
847
+ "CameraPreview",
848
+ "WebView locationInWindow: (" +
849
+ webViewLocationInWindow[0] +
850
+ ", " +
851
+ webViewLocationInWindow[1] +
852
+ ")"
853
+ );
854
+ Log.d(
855
+ "CameraPreview",
856
+ "WebView locationOnScreen: (" +
857
+ webViewLocationOnScreen[0] +
858
+ ", " +
859
+ webViewLocationOnScreen[1] +
860
+ ")"
861
+ );
862
+ Log.d(
863
+ "CameraPreview",
864
+ "Parent locationInWindow: (" +
865
+ parentLocationInWindow[0] +
866
+ ", " +
867
+ parentLocationInWindow[1] +
868
+ ")"
869
+ );
870
+ Log.d(
871
+ "CameraPreview",
872
+ "Parent locationOnScreen: (" +
873
+ parentLocationOnScreen[0] +
874
+ ", " +
875
+ parentLocationOnScreen[1] +
876
+ ")"
877
+ );
878
+ Log.d(
879
+ "CameraPreview",
880
+ "Parent class: " + webViewParent.getClass().getSimpleName()
881
+ );
882
+ Log.d(
883
+ "CameraPreview",
884
+ "Requested position (logical): (" + x + ", " + y + ")"
885
+ );
886
+ Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
887
+ Log.d(
888
+ "CameraPreview",
889
+ "Final computed position (no offset): (" +
890
+ computedX +
891
+ ", " +
892
+ computedY +
893
+ ")"
894
+ );
895
+ Log.d("CameraPreview", "5. IS_CENTERED - " + (x == -1 || y == -1));
896
+ Log.d("CameraPreview", "========================");
897
+
898
+ // Pass along whether we're centering so CameraXView knows not to add insets
899
+ boolean isCentered = (x == -1 || y == -1);
900
+
901
+ CameraSessionConfiguration config = new CameraSessionConfiguration(
902
+ finalDeviceId,
903
+ position,
904
+ computedX,
905
+ computedY,
906
+ computedWidth,
907
+ computedHeight,
908
+ paddingBottom,
909
+ toBack,
910
+ storeToFile,
911
+ enableOpacity,
912
+ enableZoom,
913
+ disableExifHeaderStripping,
914
+ disableAudio,
915
+ 1.0f,
916
+ aspectRatio,
917
+ gridMode
918
+ );
919
+ config.setTargetZoom(finalTargetZoom);
920
+ config.setCentered(isCentered);
921
+
922
+ bridge.saveCall(call);
923
+ cameraStartCallbackId = call.getCallbackId();
924
+ cameraXView.startSession(config);
925
+ });
610
926
  }
611
927
 
612
928
  @Override
613
- public void onPictureTaken(String originalPicture) {
614
- JSObject jsObject = new JSObject();
615
- jsObject.put("value", originalPicture);
616
- bridge.getSavedCall(captureCallbackId).resolve(jsObject);
929
+ public void onPictureTaken(String base64, JSONObject exif) {
930
+ PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
931
+ if (pluginCall == null) {
932
+ Log.e("CameraPreview", "onPictureTaken: captureCallbackId is null");
933
+ return;
934
+ }
935
+ JSObject result = new JSObject();
936
+ result.put("value", base64);
937
+ result.put("exif", exif);
938
+ pluginCall.resolve(result);
939
+ bridge.releaseCall(pluginCall);
617
940
  }
618
941
 
619
942
  @Override
620
943
  public void onPictureTakenError(String message) {
621
- bridge.getSavedCall(captureCallbackId).reject(message);
944
+ PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
945
+ if (pluginCall == null) {
946
+ Log.e("CameraPreview", "onPictureTakenError: captureCallbackId is null");
947
+ return;
948
+ }
949
+ pluginCall.reject(message);
950
+ bridge.releaseCall(pluginCall);
622
951
  }
623
952
 
624
953
  @Override
625
- public void onSnapshotTaken(String originalPicture) {
626
- JSObject jsObject = new JSObject();
627
- jsObject.put("value", originalPicture);
628
- bridge.getSavedCall(snapshotCallbackId).resolve(jsObject);
629
- }
954
+ public void onCameraStarted(int width, int height, int x, int y) {
955
+ PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
956
+ if (call != null) {
957
+ // Convert pixel values back to logical units
958
+ DisplayMetrics metrics = getBridge()
959
+ .getActivity()
960
+ .getResources()
961
+ .getDisplayMetrics();
962
+ float pixelRatio = metrics.density;
963
+
964
+ // When WebView is offset from the top (e.g., below status bar),
965
+ // we need to convert between JavaScript coordinates (relative to WebView)
966
+ // and native coordinates (relative to screen)
967
+ WebView webView = getBridge().getWebView();
968
+ int webViewTopInset = 0;
969
+ boolean isEdgeToEdgeActive = false;
970
+ if (webView != null) {
971
+ int[] location = new int[2];
972
+ webView.getLocationOnScreen(location);
973
+ webViewTopInset = location[1];
974
+ isEdgeToEdgeActive = webViewTopInset > 0;
975
+ }
630
976
 
631
- @Override
632
- public void onSnapshotTakenError(String message) {
633
- bridge.getSavedCall(snapshotCallbackId).reject(message);
977
+ // Only convert to relative position if edge-to-edge is active
978
+ int relativeY = isEdgeToEdgeActive ? (y - webViewTopInset) : y;
979
+
980
+ Log.d("CameraPreview", "========================");
981
+ Log.d("CameraPreview", "CAMERA STARTED - POSITION RETURNED:");
982
+ Log.d(
983
+ "CameraPreview",
984
+ "7. RETURNED (pixels) - x=" +
985
+ x +
986
+ ", y=" +
987
+ y +
988
+ ", width=" +
989
+ width +
990
+ ", height=" +
991
+ height
992
+ );
993
+ Log.d(
994
+ "CameraPreview",
995
+ "8. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE" : "INACTIVE")
996
+ );
997
+ Log.d("CameraPreview", "9. WEBVIEW INSET - " + webViewTopInset);
998
+ Log.d(
999
+ "CameraPreview",
1000
+ "10. RELATIVE Y - " +
1001
+ relativeY +
1002
+ " (y=" +
1003
+ y +
1004
+ (isEdgeToEdgeActive ? " - inset=" + webViewTopInset : " unchanged") +
1005
+ ")"
1006
+ );
1007
+ Log.d(
1008
+ "CameraPreview",
1009
+ "11. RETURNED (logical) - x=" +
1010
+ (x / pixelRatio) +
1011
+ ", y=" +
1012
+ (relativeY / pixelRatio) +
1013
+ ", width=" +
1014
+ (width / pixelRatio) +
1015
+ ", height=" +
1016
+ (height / pixelRatio)
1017
+ );
1018
+ Log.d("CameraPreview", "12. PIXEL RATIO - " + pixelRatio);
1019
+ Log.d("CameraPreview", "========================");
1020
+
1021
+ JSObject result = new JSObject();
1022
+ result.put("width", width / pixelRatio);
1023
+ result.put("height", height / pixelRatio);
1024
+ result.put("x", x / pixelRatio);
1025
+ result.put("y", relativeY / pixelRatio);
1026
+ call.resolve(result);
1027
+ bridge.releaseCall(call);
1028
+ cameraStartCallbackId = null; // Prevent re-use
1029
+ }
634
1030
  }
635
1031
 
636
1032
  @Override
637
- public void onFocusSet(int pointX, int pointY) {}
638
-
639
- @Override
640
- public void onFocusSetError(String message) {}
641
-
642
- @Override
643
- public void onBackButton() {}
644
-
645
- @Override
646
- public void onCameraStarted() {
647
- PluginCall pluginCall = bridge.getSavedCall(cameraStartCallbackId);
648
- pluginCall.resolve();
649
- bridge.releaseCall(pluginCall);
1033
+ public void onSampleTaken(String result) {
1034
+ // Handle sample taken if needed
1035
+ Log.i("CameraPreview", "Sample taken: " + result);
650
1036
  }
651
1037
 
652
1038
  @Override
653
- public void onStartRecordVideo() {}
654
-
655
- @Override
656
- public void onStartRecordVideoError(String message) {
657
- bridge.getSavedCall(recordCallbackId).reject(message);
1039
+ public void onSampleTakenError(String message) {
1040
+ // Handle sample taken error if needed
1041
+ Log.e("CameraPreview", "Sample taken error: " + message);
658
1042
  }
659
1043
 
660
1044
  @Override
661
- public void onStopRecordVideo(String file) {
662
- PluginCall pluginCall = bridge.getSavedCall(recordCallbackId);
663
- JSObject jsObject = new JSObject();
664
- jsObject.put("videoFilePath", file);
665
- pluginCall.resolve(jsObject);
1045
+ public void onCameraStartError(String message) {
1046
+ PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
1047
+ if (call != null) {
1048
+ call.reject(message);
1049
+ bridge.releaseCall(call);
1050
+ cameraStartCallbackId = null;
1051
+ }
666
1052
  }
667
1053
 
668
- @Override
669
- public void onStopRecordVideoError(String error) {
670
- bridge.getSavedCall(recordCallbackId).reject(error);
1054
+ @PluginMethod
1055
+ public void setAspectRatio(PluginCall call) {
1056
+ if (cameraXView == null || !cameraXView.isRunning()) {
1057
+ call.reject("Camera is not running");
1058
+ return;
1059
+ }
1060
+ String aspectRatio = call.getString("aspectRatio", "4:3");
1061
+ Float x = call.getFloat("x");
1062
+ Float y = call.getFloat("y");
1063
+
1064
+ getActivity()
1065
+ .runOnUiThread(() -> {
1066
+ cameraXView.setAspectRatio(aspectRatio, x, y, () -> {
1067
+ // Return the actual preview bounds after layout and camera operations are complete
1068
+ int[] bounds = cameraXView.getCurrentPreviewBounds();
1069
+ JSObject ret = new JSObject();
1070
+ ret.put("x", bounds[0]);
1071
+ ret.put("y", bounds[1]);
1072
+ ret.put("width", bounds[2]);
1073
+ ret.put("height", bounds[3]);
1074
+ call.resolve(ret);
1075
+ });
1076
+ });
671
1077
  }
672
1078
 
673
- private boolean hasView(PluginCall call) {
674
- if (fragment == null) {
675
- return false;
1079
+ @PluginMethod
1080
+ public void getAspectRatio(PluginCall call) {
1081
+ if (cameraXView == null || !cameraXView.isRunning()) {
1082
+ call.reject("Camera is not running");
1083
+ return;
676
1084
  }
1085
+ String aspectRatio = cameraXView.getAspectRatio();
1086
+ JSObject ret = new JSObject();
1087
+ ret.put("aspectRatio", aspectRatio);
1088
+ call.resolve(ret);
1089
+ }
677
1090
 
678
- return true;
1091
+ @PluginMethod
1092
+ public void setGridMode(PluginCall call) {
1093
+ if (cameraXView == null || !cameraXView.isRunning()) {
1094
+ call.reject("Camera is not running");
1095
+ return;
1096
+ }
1097
+ String gridMode = call.getString("gridMode", "none");
1098
+ getActivity()
1099
+ .runOnUiThread(() -> {
1100
+ cameraXView.setGridMode(gridMode);
1101
+ call.resolve();
1102
+ });
679
1103
  }
680
1104
 
681
- private boolean hasCamera(PluginCall call) {
682
- if (!this.hasView(call)) {
683
- return false;
1105
+ @PluginMethod
1106
+ public void getGridMode(PluginCall call) {
1107
+ if (cameraXView == null || !cameraXView.isRunning()) {
1108
+ call.reject("Camera is not running");
1109
+ return;
684
1110
  }
1111
+ JSObject ret = new JSObject();
1112
+ ret.put("gridMode", cameraXView.getGridMode());
1113
+ call.resolve(ret);
1114
+ }
685
1115
 
686
- if (fragment.getCamera() == null) {
687
- return false;
1116
+ @PluginMethod
1117
+ public void getPreviewSize(PluginCall call) {
1118
+ if (cameraXView == null || !cameraXView.isRunning()) {
1119
+ call.reject("Camera is not running");
1120
+ return;
688
1121
  }
689
1122
 
690
- return true;
1123
+ // Convert pixel values back to logical units
1124
+ DisplayMetrics metrics = getBridge()
1125
+ .getActivity()
1126
+ .getResources()
1127
+ .getDisplayMetrics();
1128
+ float pixelRatio = metrics.density;
1129
+
1130
+ JSObject ret = new JSObject();
1131
+ ret.put("x", cameraXView.getPreviewX() / pixelRatio);
1132
+ ret.put("y", cameraXView.getPreviewY() / pixelRatio);
1133
+ ret.put("width", cameraXView.getPreviewWidth() / pixelRatio);
1134
+ ret.put("height", cameraXView.getPreviewHeight() / pixelRatio);
1135
+ call.resolve(ret);
691
1136
  }
692
1137
 
693
- private String getFilePath() {
694
- String fileName = "videoTmp";
1138
+ @PluginMethod
1139
+ public void setPreviewSize(PluginCall call) {
1140
+ if (cameraXView == null || !cameraXView.isRunning()) {
1141
+ call.reject("Camera is not running");
1142
+ return;
1143
+ }
695
1144
 
696
- int i = 1;
1145
+ // Get values from call - null values will become 0
1146
+ Integer xParam = call.getInt("x");
1147
+ Integer yParam = call.getInt("y");
1148
+ Integer widthParam = call.getInt("width");
1149
+ Integer heightParam = call.getInt("height");
697
1150
 
698
- while (
699
- new File(VIDEO_FILE_PATH + fileName + VIDEO_FILE_EXTENSION).exists()
700
- ) {
701
- // Add number suffix if file exists
702
- fileName = "videoTmp" + '_' + i;
703
- i++;
1151
+ // Apply pixel ratio conversion to non-null values
1152
+ DisplayMetrics metrics = getBridge()
1153
+ .getActivity()
1154
+ .getResources()
1155
+ .getDisplayMetrics();
1156
+ float pixelRatio = metrics.density;
1157
+
1158
+ // Check if edge-to-edge mode is active
1159
+ WebView webView = getBridge().getWebView();
1160
+ int webViewTopInset = 0;
1161
+ boolean isEdgeToEdgeActive = false;
1162
+ if (webView != null) {
1163
+ int[] location = new int[2];
1164
+ webView.getLocationOnScreen(location);
1165
+ webViewTopInset = location[1];
1166
+ isEdgeToEdgeActive = webViewTopInset > 0;
704
1167
  }
705
1168
 
706
- return VIDEO_FILE_PATH + fileName + VIDEO_FILE_EXTENSION;
707
- }
708
-
709
- private void setupBroadcast() {
710
- /** When touch event is triggered, relay it to camera view if needed so it can support pinch zoom */
1169
+ int x = (xParam != null && xParam > 0) ? (int) (xParam * pixelRatio) : 0;
1170
+ int y = (yParam != null && yParam > 0) ? (int) (yParam * pixelRatio) : 0;
711
1171
 
712
- getBridge().getWebView().setClickable(true);
713
- getBridge()
714
- .getWebView()
715
- .setOnTouchListener(
716
- new View.OnTouchListener() {
717
- @Override
718
- public boolean onTouch(View v, MotionEvent event) {
719
- if (
720
- (null != fragment) &&
721
- (fragment.toBack == true) &&
722
- null != fragment.frameContainerLayout
723
- ) {
724
- fragment.frameContainerLayout.dispatchTouchEvent(event);
725
- }
726
- return false;
727
- }
728
- }
729
- );
1172
+ // Add edge-to-edge inset to Y if active
1173
+ if (isEdgeToEdgeActive && y > 0) {
1174
+ y += webViewTopInset;
1175
+ }
1176
+ int width = (widthParam != null && widthParam > 0)
1177
+ ? (int) (widthParam * pixelRatio)
1178
+ : 0;
1179
+ int height = (heightParam != null && heightParam > 0)
1180
+ ? (int) (heightParam * pixelRatio)
1181
+ : 0;
1182
+
1183
+ cameraXView.setPreviewSize(x, y, width, height, () -> {
1184
+ // Return the actual preview bounds after layout operations are complete
1185
+ int[] bounds = cameraXView.getCurrentPreviewBounds();
1186
+ JSObject ret = new JSObject();
1187
+ ret.put("x", bounds[0]);
1188
+ ret.put("y", bounds[1]);
1189
+ ret.put("width", bounds[2]);
1190
+ ret.put("height", bounds[3]);
1191
+ call.resolve(ret);
1192
+ });
730
1193
  }
731
1194
  }