@capgo/camera-preview 7.3.12 → 7.4.0-beta.10

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 (61) hide show
  1. package/CapgoCameraPreview.podspec +16 -13
  2. package/README.md +467 -73
  3. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  4. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
  5. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  6. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  7. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  8. package/android/.gradle/8.14.2/fileChanges/last-build.bin +0 -0
  9. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  10. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  11. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  12. package/android/.gradle/8.14.2/gc.properties +0 -0
  13. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  14. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  15. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  16. package/android/.gradle/file-system.probe +0 -0
  17. package/android/.gradle/vcs-1/gc.properties +0 -0
  18. package/android/build.gradle +11 -0
  19. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  20. package/android/src/main/AndroidManifest.xml +5 -3
  21. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +472 -541
  22. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +1648 -0
  23. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +82 -0
  24. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +54 -0
  25. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +70 -0
  26. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +79 -0
  27. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +34 -0
  28. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +34 -0
  29. package/dist/docs.json +934 -154
  30. package/dist/esm/definitions.d.ts +445 -83
  31. package/dist/esm/definitions.js +10 -1
  32. package/dist/esm/definitions.js.map +1 -1
  33. package/dist/esm/web.d.ts +73 -3
  34. package/dist/esm/web.js +492 -68
  35. package/dist/esm/web.js.map +1 -1
  36. package/dist/plugin.cjs.js +498 -68
  37. package/dist/plugin.cjs.js.map +1 -1
  38. package/dist/plugin.js +498 -68
  39. package/dist/plugin.js.map +1 -1
  40. package/ios/{Plugin → Sources/CapgoCameraPreview}/CameraController.swift +601 -59
  41. package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
  42. package/ios/Sources/CapgoCameraPreview/Plugin.swift +1369 -0
  43. package/ios/Tests/CameraPreviewPluginTests/CameraPreviewPluginTests.swift +15 -0
  44. package/package.json +1 -1
  45. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java +0 -1279
  46. package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java +0 -29
  47. package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java +0 -39
  48. package/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java +0 -461
  49. package/android/src/main/java/com/ahm/capacitor/camera/preview/TapGestureDetector.java +0 -24
  50. package/ios/Plugin/Info.plist +0 -24
  51. package/ios/Plugin/Plugin.h +0 -10
  52. package/ios/Plugin/Plugin.m +0 -18
  53. package/ios/Plugin/Plugin.swift +0 -511
  54. package/ios/Plugin.xcodeproj/project.pbxproj +0 -593
  55. package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  56. package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
  57. package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  58. package/ios/PluginTests/Info.plist +0 -22
  59. package/ios/PluginTests/PluginTests.swift +0 -83
  60. package/ios/Podfile +0 -13
  61. package/ios/Podfile.lock +0 -23
@@ -0,0 +1,1648 @@
1
+ package com.ahm.capacitor.camera.preview;
2
+
3
+ import android.content.Context;
4
+ import android.hardware.camera2.CameraAccessException;
5
+ import android.hardware.camera2.CameraManager;
6
+ import android.os.Build;
7
+ import android.util.Base64;
8
+ import android.util.DisplayMetrics;
9
+ import android.util.Log;
10
+ import android.util.Size;
11
+ import android.view.ViewGroup;
12
+ import android.webkit.WebView;
13
+ import androidx.annotation.NonNull;
14
+ import androidx.annotation.OptIn;
15
+ import androidx.camera.core.Camera;
16
+ import androidx.camera.core.CameraInfo;
17
+ import androidx.camera.core.CameraSelector;
18
+ import androidx.camera.core.ImageCapture;
19
+ import androidx.camera.core.ImageCaptureException;
20
+ import androidx.camera.core.ImageProxy;
21
+ import androidx.camera.core.Preview;
22
+ import androidx.camera.core.AspectRatio;
23
+ import androidx.camera.core.resolutionselector.AspectRatioStrategy;
24
+ import androidx.camera.core.resolutionselector.ResolutionSelector;
25
+ import androidx.camera.core.resolutionselector.ResolutionStrategy;
26
+ import androidx.camera.lifecycle.ProcessCameraProvider;
27
+ import androidx.camera.view.PreviewView;
28
+ import androidx.core.content.ContextCompat;
29
+ import androidx.lifecycle.Lifecycle;
30
+ import androidx.lifecycle.LifecycleOwner;
31
+ import androidx.lifecycle.LifecycleRegistry;
32
+ import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
33
+ import android.widget.FrameLayout;
34
+ import com.ahm.capacitor.camera.preview.model.LensInfo;
35
+ import com.ahm.capacitor.camera.preview.model.ZoomFactors;
36
+ import com.google.common.util.concurrent.ListenableFuture;
37
+ import java.nio.ByteBuffer;
38
+ import java.util.Arrays;
39
+ import java.util.Collections;
40
+ import java.util.List;
41
+ import java.util.ArrayList;
42
+ import java.util.Objects;
43
+ import java.util.concurrent.Executor;
44
+ import java.util.concurrent.ExecutorService;
45
+ import java.util.concurrent.Executors;
46
+ import androidx.camera.camera2.interop.Camera2CameraInfo;
47
+ import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
48
+ import android.hardware.camera2.CameraCharacteristics;
49
+ import java.util.Set;
50
+ import androidx.camera.core.ZoomState;
51
+ import androidx.camera.core.ResolutionInfo;
52
+ import android.content.Intent;
53
+ import android.net.Uri;
54
+ import android.os.Environment;
55
+ import java.io.File;
56
+ import java.io.FileOutputStream;
57
+ import java.io.IOException;
58
+ import java.text.SimpleDateFormat;
59
+ import java.util.Locale;
60
+ import androidx.exifinterface.media.ExifInterface;
61
+ import org.json.JSONObject;
62
+ import java.nio.file.Files;
63
+ import android.graphics.BitmapFactory;
64
+ import android.graphics.Bitmap;
65
+ import android.graphics.Matrix;
66
+ import java.io.ByteArrayOutputStream;
67
+ import android.location.Location;
68
+ import android.widget.FrameLayout;
69
+ import androidx.lifecycle.LifecycleObserver;
70
+ import androidx.lifecycle.OnLifecycleEvent;
71
+ import android.util.Rational;
72
+ import android.view.ViewGroup;
73
+
74
+ public class CameraXView implements LifecycleOwner, LifecycleObserver {
75
+ private static final String TAG = "CameraPreview CameraXView";
76
+
77
+ public interface CameraXViewListener {
78
+ void onPictureTaken(String base64, JSONObject exif);
79
+ void onPictureTakenError(String message);
80
+ void onSampleTaken(String result);
81
+ void onSampleTakenError(String message);
82
+ void onCameraStarted(int width, int height, int x, int y);
83
+ void onCameraStartError(String message);
84
+ }
85
+
86
+ private ProcessCameraProvider cameraProvider;
87
+ private Camera camera;
88
+ private ImageCapture imageCapture;
89
+ private ImageCapture sampleImageCapture;
90
+ private PreviewView previewView;
91
+ private GridOverlayView gridOverlayView;
92
+ private FrameLayout previewContainer;
93
+ private CameraSelector currentCameraSelector;
94
+ private String currentDeviceId;
95
+ private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
96
+ private CameraSessionConfiguration sessionConfig;
97
+ private CameraXViewListener listener;
98
+ private final Context context;
99
+ private final WebView webView;
100
+ private final LifecycleRegistry lifecycleRegistry;
101
+ private final Executor mainExecutor;
102
+ private ExecutorService cameraExecutor;
103
+ private boolean isRunning = false;
104
+
105
+ public CameraXView(Context context, WebView webView) {
106
+ this.context = context;
107
+ this.webView = webView;
108
+ this.lifecycleRegistry = new LifecycleRegistry(this);
109
+ this.mainExecutor = ContextCompat.getMainExecutor(context);
110
+
111
+ mainExecutor.execute(() -> lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED));
112
+ }
113
+
114
+ @NonNull
115
+ @Override
116
+ public Lifecycle getLifecycle() {
117
+ return lifecycleRegistry;
118
+ }
119
+
120
+ public void setListener(CameraXViewListener listener) {
121
+ this.listener = listener;
122
+ }
123
+
124
+ public boolean isRunning() {
125
+ return isRunning;
126
+ }
127
+
128
+ private void saveImageToGallery(byte[] data) {
129
+ try {
130
+ File photo = new File(
131
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
132
+ "IMG_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new java.util.Date()) + ".jpg"
133
+ );
134
+ FileOutputStream fos = new FileOutputStream(photo);
135
+ fos.write(data);
136
+ fos.close();
137
+
138
+ // Notify the gallery of the new image
139
+ Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
140
+ Uri contentUri = Uri.fromFile(photo);
141
+ mediaScanIntent.setData(contentUri);
142
+ context.sendBroadcast(mediaScanIntent);
143
+ } catch (IOException e) {
144
+ Log.e(TAG, "Error saving image to gallery", e);
145
+ }
146
+ }
147
+
148
+ public void startSession(CameraSessionConfiguration config) {
149
+ this.sessionConfig = config;
150
+ cameraExecutor = Executors.newSingleThreadExecutor();
151
+ mainExecutor.execute(() -> {
152
+ lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
153
+ setupCamera();
154
+ });
155
+ }
156
+
157
+ public void stopSession() {
158
+ isRunning = false;
159
+ mainExecutor.execute(() -> {
160
+ if (cameraProvider != null) {
161
+ cameraProvider.unbindAll();
162
+ }
163
+ lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
164
+ if (cameraExecutor != null) {
165
+ cameraExecutor.shutdownNow();
166
+ }
167
+ removePreviewView();
168
+ });
169
+ }
170
+
171
+ private void setupCamera() {
172
+ ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
173
+ cameraProviderFuture.addListener(() -> {
174
+ try {
175
+ cameraProvider = cameraProviderFuture.get();
176
+ setupPreviewView();
177
+ bindCameraUseCases();
178
+ } catch (Exception e) {
179
+ if (listener != null) {
180
+ listener.onCameraStartError("Error initializing camera: " + e.getMessage());
181
+ }
182
+ }
183
+ }, mainExecutor);
184
+ }
185
+
186
+ private void setupPreviewView() {
187
+ if (previewView != null) {
188
+ removePreviewView();
189
+ }
190
+ if (sessionConfig.isToBack()) {
191
+ webView.setBackgroundColor(android.graphics.Color.TRANSPARENT);
192
+ }
193
+
194
+ // Create a container to hold both the preview and grid overlay
195
+ previewContainer = new FrameLayout(context);
196
+
197
+ // Create and setup the preview view
198
+ previewView = new PreviewView(context);
199
+ previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
200
+ previewContainer.addView(previewView, new FrameLayout.LayoutParams(
201
+ FrameLayout.LayoutParams.MATCH_PARENT,
202
+ FrameLayout.LayoutParams.MATCH_PARENT
203
+ ));
204
+
205
+ // Create and setup the grid overlay
206
+ gridOverlayView = new GridOverlayView(context);
207
+ previewContainer.addView(gridOverlayView, new FrameLayout.LayoutParams(
208
+ FrameLayout.LayoutParams.MATCH_PARENT,
209
+ FrameLayout.LayoutParams.MATCH_PARENT
210
+ ));
211
+ // Set grid mode after adding to container to ensure proper layout
212
+ gridOverlayView.post(() -> {
213
+ String currentGridMode = sessionConfig.getGridMode();
214
+ Log.d(TAG, "setupPreviewView: Setting grid mode to: " + currentGridMode);
215
+ gridOverlayView.setGridMode(currentGridMode);
216
+ });
217
+
218
+ ViewGroup parent = (ViewGroup) webView.getParent();
219
+ if (parent != null) {
220
+ FrameLayout.LayoutParams layoutParams = calculatePreviewLayoutParams();
221
+ parent.addView(previewContainer, layoutParams);
222
+ if(sessionConfig.isToBack()) webView.bringToFront();
223
+ }
224
+ }
225
+
226
+ private FrameLayout.LayoutParams calculatePreviewLayoutParams() {
227
+ // sessionConfig already contains pixel-converted coordinates with webview offsets applied
228
+ int x = sessionConfig.getX();
229
+ int y = sessionConfig.getY();
230
+ int width = sessionConfig.getWidth();
231
+ int height = sessionConfig.getHeight();
232
+ String aspectRatio = sessionConfig.getAspectRatio();
233
+
234
+ Log.d(TAG, "calculatePreviewLayoutParams: Using sessionConfig values - x:" + x + " y:" + y + " width:" + width + " height:" + height + " aspectRatio:" + aspectRatio);
235
+
236
+ // Apply aspect ratio if specified and no explicit size was given
237
+ if (aspectRatio != null && !aspectRatio.isEmpty()) {
238
+ String[] ratios = aspectRatio.split(":");
239
+ if (ratios.length == 2) {
240
+ try {
241
+ // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
242
+ float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
243
+
244
+ // Calculate optimal size while maintaining aspect ratio
245
+ int optimalWidth = width;
246
+ int optimalHeight = (int) (width / ratio);
247
+
248
+ if (optimalHeight > height) {
249
+ // Height constraint is tighter, fit by height
250
+ optimalHeight = height;
251
+ optimalWidth = (int) (height * ratio);
252
+ }
253
+
254
+ width = optimalWidth;
255
+ height = optimalHeight;
256
+ Log.d(TAG, "calculatePreviewLayoutParams: Applied aspect ratio " + aspectRatio + " - new size: " + width + "x" + height);
257
+ } catch (NumberFormatException e) {
258
+ Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
259
+ }
260
+ }
261
+ }
262
+
263
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
264
+
265
+ // Only add insets for positioning coordinates, not for full-screen sizes
266
+ int webViewTopInset = getWebViewTopInset();
267
+ int webViewLeftInset = getWebViewLeftInset();
268
+
269
+ // Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
270
+ if (x == 0 && y == 0) {
271
+ layoutParams.leftMargin = x;
272
+ layoutParams.topMargin = y;
273
+ Log.d(TAG, "calculatePreviewLayoutParams: Full-screen mode - keeping position (0,0) without insets");
274
+ } else {
275
+ layoutParams.leftMargin = x + webViewLeftInset;
276
+ layoutParams.topMargin = y + webViewTopInset;
277
+ Log.d(TAG, "calculatePreviewLayoutParams: Positioned mode - applying insets");
278
+ }
279
+
280
+ Log.d(TAG, "calculatePreviewLayoutParams: Applied insets - x:" + x + "+" + webViewLeftInset + "=" + layoutParams.leftMargin +
281
+ ", y:" + y + "+" + webViewTopInset + "=" + layoutParams.topMargin);
282
+
283
+ Log.d(TAG, "calculatePreviewLayoutParams: Final layout - x:" + x + " y:" + y + " width:" + width + " height:" + height);
284
+ return layoutParams;
285
+ }
286
+
287
+ private void removePreviewView() {
288
+ if (previewContainer != null) {
289
+ ViewGroup parent = (ViewGroup) previewContainer.getParent();
290
+ if (parent != null) {
291
+ parent.removeView(previewContainer);
292
+ }
293
+ previewContainer = null;
294
+ }
295
+ if (previewView != null) {
296
+ previewView = null;
297
+ }
298
+ if (gridOverlayView != null) {
299
+ gridOverlayView = null;
300
+ }
301
+ webView.setBackgroundColor(android.graphics.Color.WHITE);
302
+ }
303
+
304
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
305
+ private void bindCameraUseCases() {
306
+ if (cameraProvider == null) return;
307
+ mainExecutor.execute(() -> {
308
+ try {
309
+ Log.d(TAG, "Building camera selector with deviceId: " + sessionConfig.getDeviceId() + " and position: " + sessionConfig.getPosition());
310
+ currentCameraSelector = buildCameraSelector();
311
+
312
+ ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder()
313
+ .setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY);
314
+
315
+ if (sessionConfig.getAspectRatio() != null) {
316
+ int aspectRatio;
317
+ if ("16:9".equals(sessionConfig.getAspectRatio())) {
318
+ aspectRatio = AspectRatio.RATIO_16_9;
319
+ } else { // "4:3"
320
+ aspectRatio = AspectRatio.RATIO_4_3;
321
+ }
322
+ resolutionSelectorBuilder.setAspectRatioStrategy(new AspectRatioStrategy(aspectRatio, AspectRatioStrategy.FALLBACK_RULE_AUTO));
323
+ }
324
+
325
+ ResolutionSelector resolutionSelector = resolutionSelectorBuilder.build();
326
+
327
+ Preview preview = new Preview.Builder().setResolutionSelector(resolutionSelector).build();
328
+ imageCapture = new ImageCapture.Builder().setResolutionSelector(resolutionSelector).setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).setFlashMode(currentFlashMode).build();
329
+ sampleImageCapture = imageCapture;
330
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
331
+ // Unbind any existing use cases and bind new ones
332
+ cameraProvider.unbindAll();
333
+ camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture);
334
+
335
+ // Log details about the active camera
336
+ Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
337
+ CameraInfo cameraInfo = camera.getCameraInfo();
338
+ Log.d(TAG, "Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId());
339
+
340
+ // Log zoom state
341
+ ZoomState zoomState = cameraInfo.getZoomState().getValue();
342
+ if (zoomState != null) {
343
+ Log.d(TAG, "Active Zoom State: " +
344
+ "min=" + zoomState.getMinZoomRatio() + ", " +
345
+ "max=" + zoomState.getMaxZoomRatio() + ", " +
346
+ "current=" + zoomState.getZoomRatio());
347
+ }
348
+
349
+ // Log physical cameras of the active camera
350
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
351
+ Set<CameraInfo> physicalCameras = cameraInfo.getPhysicalCameraInfos();
352
+ Log.d(TAG, "Active camera has " + physicalCameras.size() + " physical cameras.");
353
+ for (CameraInfo physical : physicalCameras) {
354
+ Log.d(TAG, " - Physical camera ID: " + Camera2CameraInfo.from(physical).getCameraId());
355
+ }
356
+ }
357
+
358
+ // Log resolution info
359
+ ResolutionInfo previewResolution = preview.getResolutionInfo();
360
+ if (previewResolution != null) {
361
+ Log.d(TAG, "Preview resolution: " + previewResolution.getResolution());
362
+ }
363
+ ResolutionInfo imageCaptureResolution = imageCapture.getResolutionInfo();
364
+ if (imageCaptureResolution != null) {
365
+ Log.d(TAG, "Image capture resolution: " + imageCaptureResolution.getResolution());
366
+ }
367
+
368
+ // Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
369
+ float initialZoom = sessionConfig.getTargetZoom() != 1.0f ? sessionConfig.getTargetZoom() : sessionConfig.getZoomFactor();
370
+ if (initialZoom != 1.0f) {
371
+ Log.d(TAG, "Applying initial zoom of " + initialZoom);
372
+ setZoomInternal(initialZoom);
373
+ }
374
+
375
+ isRunning = true;
376
+ Log.d(TAG, "bindCameraUseCases: Camera bound successfully");
377
+ if (listener != null) {
378
+ // Post the callback to ensure layout is complete
379
+ previewContainer.post(() -> {
380
+ // Return actual preview container dimensions instead of requested dimensions
381
+ int actualWidth = previewContainer != null ? previewContainer.getWidth() : sessionConfig.getWidth();
382
+ int actualHeight = previewContainer != null ? previewContainer.getHeight() : sessionConfig.getHeight();
383
+ int actualX = previewContainer != null ? previewContainer.getLeft() : sessionConfig.getX();
384
+ int actualY = previewContainer != null ? previewContainer.getTop() : sessionConfig.getY();
385
+ listener.onCameraStarted(actualWidth, actualHeight, actualX, actualY);
386
+ });
387
+ }
388
+ } catch (Exception e) {
389
+ if (listener != null) listener.onCameraStartError("Error binding camera: " + e.getMessage());
390
+ }
391
+ });
392
+ }
393
+
394
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
395
+ private CameraSelector buildCameraSelector() {
396
+ CameraSelector.Builder builder = new CameraSelector.Builder();
397
+ final String deviceId = sessionConfig.getDeviceId();
398
+
399
+ if (deviceId != null && !deviceId.isEmpty()) {
400
+ builder.addCameraFilter(cameraInfos -> {
401
+ for (CameraInfo cameraInfo : cameraInfos) {
402
+ if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
403
+ return Collections.singletonList(cameraInfo);
404
+ }
405
+ }
406
+ return Collections.emptyList();
407
+ });
408
+ } else {
409
+ String position = sessionConfig.getPosition();
410
+ int requiredFacing = "front".equals(position) ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
411
+ builder.requireLensFacing(requiredFacing);
412
+ }
413
+ return builder.build();
414
+ }
415
+
416
+ private static String getCameraId(androidx.camera.core.CameraInfo cameraInfo) {
417
+ try {
418
+ // Generate a stable ID based on camera characteristics
419
+ boolean isBack = isBackCamera(cameraInfo);
420
+ float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
421
+ float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
422
+
423
+ // Create a unique ID based on camera properties
424
+ String position = isBack ? "back" : "front";
425
+ return position + "_" + minZoom + "_" + maxZoom;
426
+ } catch (Exception e) {
427
+ return "unknown_camera";
428
+ }
429
+ }
430
+
431
+ private static boolean isBackCamera(androidx.camera.core.CameraInfo cameraInfo) {
432
+ try {
433
+ // Check if this camera matches the back camera selector
434
+ CameraSelector backSelector = new CameraSelector.Builder()
435
+ .requireLensFacing(CameraSelector.LENS_FACING_BACK)
436
+ .build();
437
+
438
+ // Try to filter cameras with back selector - if this camera is included, it's a back camera
439
+ List<androidx.camera.core.CameraInfo> backCameras = backSelector.filter(Collections.singletonList(cameraInfo));
440
+ return !backCameras.isEmpty();
441
+ } catch (Exception e) {
442
+ Log.w(TAG, "Error determining camera direction, assuming back camera", e);
443
+ return true; // Default to back camera
444
+ }
445
+ }
446
+
447
+ public void capturePhoto(int quality, final boolean saveToGallery, Integer width, Integer height, Location location) {
448
+ Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality);
449
+
450
+ if (imageCapture == null) {
451
+ if (listener != null) {
452
+ listener.onPictureTakenError("Camera not ready");
453
+ }
454
+ return;
455
+ }
456
+
457
+ File tempFile = new File(context.getCacheDir(), "temp_image.jpg");
458
+ ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(tempFile).build();
459
+
460
+ imageCapture.takePicture(
461
+ outputFileOptions,
462
+ cameraExecutor,
463
+ new ImageCapture.OnImageSavedCallback() {
464
+ @Override
465
+ public void onError(@NonNull ImageCaptureException exception) {
466
+ Log.e(TAG, "capturePhoto: Photo capture failed", exception);
467
+ if (listener != null) {
468
+ listener.onPictureTakenError("Photo capture failed: " + exception.getMessage());
469
+ }
470
+ }
471
+
472
+ @Override
473
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
474
+ try {
475
+ // Read file using FileInputStream for compatibility
476
+ byte[] bytes = new byte[(int) tempFile.length()];
477
+ java.io.FileInputStream fis = new java.io.FileInputStream(tempFile);
478
+ fis.read(bytes);
479
+ fis.close();
480
+
481
+ ExifInterface exifInterface = new ExifInterface(tempFile.getAbsolutePath());
482
+
483
+ if (location != null) {
484
+ exifInterface.setGpsInfo(location);
485
+ }
486
+
487
+ JSONObject exifData = getExifData(exifInterface);
488
+
489
+ if (width != null && height != null) {
490
+ Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
491
+ Bitmap resizedBitmap = resizeBitmap(bitmap, width, height);
492
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
493
+ resizedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
494
+ bytes = stream.toByteArray();
495
+
496
+ // Write EXIF data back to resized image
497
+ bytes = writeExifToImageBytes(bytes, exifInterface);
498
+ } else {
499
+ // For non-resized images, ensure EXIF is saved
500
+ exifInterface.saveAttributes();
501
+ bytes = new byte[(int) tempFile.length()];
502
+ java.io.FileInputStream fis2 = new java.io.FileInputStream(tempFile);
503
+ fis2.read(bytes);
504
+ fis2.close();
505
+ }
506
+
507
+ if (saveToGallery) {
508
+ saveImageToGallery(bytes);
509
+ }
510
+
511
+ String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
512
+
513
+ tempFile.delete();
514
+
515
+ if (listener != null) {
516
+ listener.onPictureTaken(base64, exifData);
517
+ }
518
+ } catch (Exception e) {
519
+ Log.e(TAG, "capturePhoto: Error processing image", e);
520
+ if (listener != null) {
521
+ listener.onPictureTakenError("Error processing image: " + e.getMessage());
522
+ }
523
+ }
524
+ }
525
+ }
526
+ );
527
+ }
528
+
529
+ private Bitmap resizeBitmap(Bitmap bitmap, int width, int height) {
530
+ return Bitmap.createScaledBitmap(bitmap, width, height, true);
531
+ }
532
+
533
+ private JSONObject getExifData(ExifInterface exifInterface) {
534
+ JSONObject exifData = new JSONObject();
535
+ try {
536
+ // Add all available exif tags to a JSON object
537
+ for (String[] tag : EXIF_TAGS) {
538
+ String value = exifInterface.getAttribute(tag[0]);
539
+ if (value != null) {
540
+ exifData.put(tag[1], value);
541
+ }
542
+ }
543
+ } catch (Exception e) {
544
+ Log.e(TAG, "getExifData: Error reading exif data", e);
545
+ }
546
+ return exifData;
547
+ }
548
+
549
+ private static final String[][] EXIF_TAGS = new String[][]{
550
+ {ExifInterface.TAG_APERTURE_VALUE, "ApertureValue"},
551
+ {ExifInterface.TAG_ARTIST, "Artist"},
552
+ {ExifInterface.TAG_BITS_PER_SAMPLE, "BitsPerSample"},
553
+ {ExifInterface.TAG_BRIGHTNESS_VALUE, "BrightnessValue"},
554
+ {ExifInterface.TAG_CFA_PATTERN, "CFAPattern"},
555
+ {ExifInterface.TAG_COLOR_SPACE, "ColorSpace"},
556
+ {ExifInterface.TAG_COMPONENTS_CONFIGURATION, "ComponentsConfiguration"},
557
+ {ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, "CompressedBitsPerPixel"},
558
+ {ExifInterface.TAG_COMPRESSION, "Compression"},
559
+ {ExifInterface.TAG_CONTRAST, "Contrast"},
560
+ {ExifInterface.TAG_COPYRIGHT, "Copyright"},
561
+ {ExifInterface.TAG_CUSTOM_RENDERED, "CustomRendered"},
562
+ {ExifInterface.TAG_DATETIME, "DateTime"},
563
+ {ExifInterface.TAG_DATETIME_DIGITIZED, "DateTimeDigitized"},
564
+ {ExifInterface.TAG_DATETIME_ORIGINAL, "DateTimeOriginal"},
565
+ {ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, "DeviceSettingDescription"},
566
+ {ExifInterface.TAG_DIGITAL_ZOOM_RATIO, "DigitalZoomRatio"},
567
+ {ExifInterface.TAG_DNG_VERSION, "DNGVersion"},
568
+ {ExifInterface.TAG_EXIF_VERSION, "ExifVersion"},
569
+ {ExifInterface.TAG_EXPOSURE_BIAS_VALUE, "ExposureBiasValue"},
570
+ {ExifInterface.TAG_EXPOSURE_INDEX, "ExposureIndex"},
571
+ {ExifInterface.TAG_EXPOSURE_MODE, "ExposureMode"},
572
+ {ExifInterface.TAG_EXPOSURE_PROGRAM, "ExposureProgram"},
573
+ {ExifInterface.TAG_EXPOSURE_TIME, "ExposureTime"},
574
+ {ExifInterface.TAG_FILE_SOURCE, "FileSource"},
575
+ {ExifInterface.TAG_FLASH, "Flash"},
576
+ {ExifInterface.TAG_FLASHPIX_VERSION, "FlashpixVersion"},
577
+ {ExifInterface.TAG_FLASH_ENERGY, "FlashEnergy"},
578
+ {ExifInterface.TAG_FOCAL_LENGTH, "FocalLength"},
579
+ {ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, "FocalLengthIn35mmFilm"},
580
+ {ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, "FocalPlaneResolutionUnit"},
581
+ {ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, "FocalPlaneXResolution"},
582
+ {ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, "FocalPlaneYResolution"},
583
+ {ExifInterface.TAG_F_NUMBER, "FNumber"},
584
+ {ExifInterface.TAG_GAIN_CONTROL, "GainControl"},
585
+ {ExifInterface.TAG_GPS_ALTITUDE, "GPSAltitude"},
586
+ {ExifInterface.TAG_GPS_ALTITUDE_REF, "GPSAltitudeRef"},
587
+ {ExifInterface.TAG_GPS_AREA_INFORMATION, "GPSAreaInformation"},
588
+ {ExifInterface.TAG_GPS_DATESTAMP, "GPSDateStamp"},
589
+ {ExifInterface.TAG_GPS_DEST_BEARING, "GPSDestBearing"},
590
+ {ExifInterface.TAG_GPS_DEST_BEARING_REF, "GPSDestBearingRef"},
591
+ {ExifInterface.TAG_GPS_DEST_DISTANCE, "GPSDestDistance"},
592
+ {ExifInterface.TAG_GPS_DEST_DISTANCE_REF, "GPSDestDistanceRef"},
593
+ {ExifInterface.TAG_GPS_DEST_LATITUDE, "GPSDestLatitude"},
594
+ {ExifInterface.TAG_GPS_DEST_LATITUDE_REF, "GPSDestLatitudeRef"},
595
+ {ExifInterface.TAG_GPS_DEST_LONGITUDE, "GPSDestLongitude"},
596
+ {ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, "GPSDestLongitudeRef"},
597
+ {ExifInterface.TAG_GPS_DIFFERENTIAL, "GPSDifferential"},
598
+ {ExifInterface.TAG_GPS_DOP, "GPSDOP"},
599
+ {ExifInterface.TAG_GPS_IMG_DIRECTION, "GPSImgDirection"},
600
+ {ExifInterface.TAG_GPS_IMG_DIRECTION_REF, "GPSImgDirectionRef"},
601
+ {ExifInterface.TAG_GPS_LATITUDE, "GPSLatitude"},
602
+ {ExifInterface.TAG_GPS_LATITUDE_REF, "GPSLatitudeRef"},
603
+ {ExifInterface.TAG_GPS_LONGITUDE, "GPSLongitude"},
604
+ {ExifInterface.TAG_GPS_LONGITUDE_REF, "GPSLongitudeRef"},
605
+ {ExifInterface.TAG_GPS_MAP_DATUM, "GPSMapDatum"},
606
+ {ExifInterface.TAG_GPS_MEASURE_MODE, "GPSMeasureMode"},
607
+ {ExifInterface.TAG_GPS_PROCESSING_METHOD, "GPSProcessingMethod"},
608
+ {ExifInterface.TAG_GPS_SATELLITES, "GPSSatellites"},
609
+ {ExifInterface.TAG_GPS_SPEED, "GPSSpeed"},
610
+ {ExifInterface.TAG_GPS_SPEED_REF, "GPSSpeedRef"},
611
+ {ExifInterface.TAG_GPS_STATUS, "GPSStatus"},
612
+ {ExifInterface.TAG_GPS_TIMESTAMP, "GPSTimeStamp"},
613
+ {ExifInterface.TAG_GPS_TRACK, "GPSTrack"},
614
+ {ExifInterface.TAG_GPS_TRACK_REF, "GPSTrackRef"},
615
+ {ExifInterface.TAG_GPS_VERSION_ID, "GPSVersionID"},
616
+ {ExifInterface.TAG_IMAGE_DESCRIPTION, "ImageDescription"},
617
+ {ExifInterface.TAG_IMAGE_LENGTH, "ImageLength"},
618
+ {ExifInterface.TAG_IMAGE_UNIQUE_ID, "ImageUniqueID"},
619
+ {ExifInterface.TAG_IMAGE_WIDTH, "ImageWidth"},
620
+ {ExifInterface.TAG_INTEROPERABILITY_INDEX, "InteroperabilityIndex"},
621
+ {ExifInterface.TAG_ISO_SPEED, "ISOSpeed"},
622
+ {ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, "ISOSpeedLatitudeyyy"},
623
+ {ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, "ISOSpeedLatitudezzz"},
624
+ {ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, "JPEGInterchangeFormat"},
625
+ {ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, "JPEGInterchangeFormatLength"},
626
+ {ExifInterface.TAG_LIGHT_SOURCE, "LightSource"},
627
+ {ExifInterface.TAG_MAKE, "Make"},
628
+ {ExifInterface.TAG_MAKER_NOTE, "MakerNote"},
629
+ {ExifInterface.TAG_MAX_APERTURE_VALUE, "MaxApertureValue"},
630
+ {ExifInterface.TAG_METERING_MODE, "MeteringMode"},
631
+ {ExifInterface.TAG_MODEL, "Model"},
632
+ {ExifInterface.TAG_NEW_SUBFILE_TYPE, "NewSubfileType"},
633
+ {ExifInterface.TAG_OECF, "OECF"},
634
+ {ExifInterface.TAG_OFFSET_TIME, "OffsetTime"},
635
+ {ExifInterface.TAG_OFFSET_TIME_DIGITIZED, "OffsetTimeDigitized"},
636
+ {ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "OffsetTimeOriginal"},
637
+ {ExifInterface.TAG_ORF_ASPECT_FRAME, "ORFAspectFrame"},
638
+ {ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH, "ORFPreviewImageLength"},
639
+ {ExifInterface.TAG_ORF_PREVIEW_IMAGE_START, "ORFPreviewImageStart"},
640
+ {ExifInterface.TAG_ORF_THUMBNAIL_IMAGE, "ORFThumbnailImage"},
641
+ {ExifInterface.TAG_ORIENTATION, "Orientation"},
642
+ {ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation"},
643
+ {ExifInterface.TAG_PIXEL_X_DIMENSION, "PixelXDimension"},
644
+ {ExifInterface.TAG_PIXEL_Y_DIMENSION, "PixelYDimension"},
645
+ {ExifInterface.TAG_PLANAR_CONFIGURATION, "PlanarConfiguration"},
646
+ {ExifInterface.TAG_PRIMARY_CHROMATICITIES, "PrimaryChromaticities"},
647
+ {ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, "RecommendedExposureIndex"},
648
+ {ExifInterface.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite"},
649
+ {ExifInterface.TAG_RELATED_SOUND_FILE, "RelatedSoundFile"},
650
+ {ExifInterface.TAG_RESOLUTION_UNIT, "ResolutionUnit"},
651
+ {ExifInterface.TAG_ROWS_PER_STRIP, "RowsPerStrip"},
652
+ {ExifInterface.TAG_RW2_ISO, "RW2ISO"},
653
+ {ExifInterface.TAG_RW2_JPG_FROM_RAW, "RW2JpgFromRaw"},
654
+ {ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER, "RW2SensorBottomBorder"},
655
+ {ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER, "RW2SensorLeftBorder"},
656
+ {ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, "RW2SensorRightBorder"},
657
+ {ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, "RW2SensorTopBorder"},
658
+ {ExifInterface.TAG_SAMPLES_PER_PIXEL, "SamplesPerPixel"},
659
+ {ExifInterface.TAG_SATURATION, "Saturation"},
660
+ {ExifInterface.TAG_SCENE_CAPTURE_TYPE, "SceneCaptureType"},
661
+ {ExifInterface.TAG_SCENE_TYPE, "SceneType"},
662
+ {ExifInterface.TAG_SENSING_METHOD, "SensingMethod"},
663
+ {ExifInterface.TAG_SENSITIVITY_TYPE, "SensitivityType"},
664
+ {ExifInterface.TAG_SHARPNESS, "Sharpness"},
665
+ {ExifInterface.TAG_SHUTTER_SPEED_VALUE, "ShutterSpeedValue"},
666
+ {ExifInterface.TAG_SOFTWARE, "Software"},
667
+ {ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, "SpatialFrequencyResponse"},
668
+ {ExifInterface.TAG_SPECTRAL_SENSITIVITY, "SpectralSensitivity"},
669
+ {ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, "StandardOutputSensitivity"},
670
+ {ExifInterface.TAG_STRIP_BYTE_COUNTS, "StripByteCounts"},
671
+ {ExifInterface.TAG_STRIP_OFFSETS, "StripOffsets"},
672
+ {ExifInterface.TAG_SUBFILE_TYPE, "SubfileType"},
673
+ {ExifInterface.TAG_SUBJECT_AREA, "SubjectArea"},
674
+ {ExifInterface.TAG_SUBJECT_DISTANCE, "SubjectDistance"},
675
+ {ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, "SubjectDistanceRange"},
676
+ {ExifInterface.TAG_SUBJECT_LOCATION, "SubjectLocation"},
677
+ {ExifInterface.TAG_SUBSEC_TIME, "SubSecTime"},
678
+ {ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, "SubSecTimeDigitized"},
679
+ {ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, "SubSecTimeOriginal"},
680
+ {ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH, "ThumbnailImageLength"},
681
+ {ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH, "ThumbnailImageWidth"},
682
+ {ExifInterface.TAG_TRANSFER_FUNCTION, "TransferFunction"},
683
+ {ExifInterface.TAG_USER_COMMENT, "UserComment"},
684
+ {ExifInterface.TAG_WHITE_BALANCE, "WhiteBalance"},
685
+ {ExifInterface.TAG_WHITE_POINT, "WhitePoint"},
686
+ {ExifInterface.TAG_X_RESOLUTION, "XResolution"},
687
+ {ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, "YCbCrCoefficients"},
688
+ {ExifInterface.TAG_Y_CB_CR_POSITIONING, "YCbCrPositioning"},
689
+ {ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, "YCbCrSubSampling"},
690
+ {ExifInterface.TAG_Y_RESOLUTION, "YResolution"}
691
+ };
692
+
693
+ private byte[] writeExifToImageBytes(byte[] imageBytes, ExifInterface sourceExif) {
694
+ try {
695
+ // Create a temporary file to write the image with EXIF
696
+ File tempExifFile = File.createTempFile("temp_exif", ".jpg", context.getCacheDir());
697
+
698
+ // Write the image bytes to temp file
699
+ java.io.FileOutputStream fos = new java.io.FileOutputStream(tempExifFile);
700
+ fos.write(imageBytes);
701
+ fos.close();
702
+
703
+ // Create new ExifInterface for the temp file and copy all EXIF data
704
+ ExifInterface newExif = new ExifInterface(tempExifFile.getAbsolutePath());
705
+
706
+ // Copy all EXIF attributes from source to new
707
+ for (String[] tag : EXIF_TAGS) {
708
+ String value = sourceExif.getAttribute(tag[0]);
709
+ if (value != null) {
710
+ newExif.setAttribute(tag[0], value);
711
+ }
712
+ }
713
+
714
+ // Save the EXIF data
715
+ newExif.saveAttributes();
716
+
717
+ // Read the file back with EXIF embedded
718
+ byte[] result = new byte[(int) tempExifFile.length()];
719
+ java.io.FileInputStream fis = new java.io.FileInputStream(tempExifFile);
720
+ fis.read(result);
721
+ fis.close();
722
+
723
+ // Clean up temp file
724
+ tempExifFile.delete();
725
+
726
+ return result;
727
+ } catch (Exception e) {
728
+ Log.e(TAG, "writeExifToImageBytes: Error writing EXIF data", e);
729
+ return imageBytes; // Return original bytes if error
730
+ }
731
+ }
732
+
733
+ public void captureSample(int quality) {
734
+ Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality);
735
+
736
+ if (sampleImageCapture == null) {
737
+ if (listener != null) {
738
+ listener.onSampleTakenError("Camera not ready");
739
+ }
740
+ return;
741
+ }
742
+
743
+ sampleImageCapture.takePicture(
744
+ cameraExecutor,
745
+ new ImageCapture.OnImageCapturedCallback() {
746
+ @Override
747
+ public void onError(@NonNull ImageCaptureException exception) {
748
+ Log.e(TAG, "captureSample: Sample capture failed", exception);
749
+ if (listener != null) {
750
+ listener.onSampleTakenError("Sample capture failed: " + exception.getMessage());
751
+ }
752
+ }
753
+
754
+ @Override
755
+ public void onCaptureSuccess(@NonNull ImageProxy image) {
756
+ try {
757
+ // Convert ImageProxy to byte array
758
+ byte[] bytes = imageProxyToByteArray(image);
759
+ String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
760
+
761
+ if (listener != null) {
762
+ listener.onSampleTaken(base64);
763
+ }
764
+ } catch (Exception e) {
765
+ Log.e(TAG, "captureSample: Error processing sample", e);
766
+ if (listener != null) {
767
+ listener.onSampleTakenError("Error processing sample: " + e.getMessage());
768
+ }
769
+ } finally {
770
+ image.close();
771
+ }
772
+ }
773
+ }
774
+ );
775
+ }
776
+
777
+ private byte[] imageProxyToByteArray(ImageProxy image) {
778
+ ImageProxy.PlaneProxy[] planes = image.getPlanes();
779
+ ByteBuffer buffer = planes[0].getBuffer();
780
+ byte[] bytes = new byte[buffer.remaining()];
781
+ buffer.get(bytes);
782
+ return bytes;
783
+ }
784
+
785
+ // not workin for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
786
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
787
+ public static List<com.ahm.capacitor.camera.preview.model.CameraDevice> getAvailableDevicesStatic(Context context) {
788
+ Log.d(TAG, "getAvailableDevicesStatic: Starting CameraX device enumeration with getPhysicalCameraInfos.");
789
+ List<com.ahm.capacitor.camera.preview.model.CameraDevice> devices = new ArrayList<>();
790
+ try {
791
+ ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
792
+ ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
793
+ CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
794
+
795
+ for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
796
+ String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
797
+ String position = isBackCamera(cameraInfo) ? "rear" : "front";
798
+
799
+ // Add logical camera
800
+ float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
801
+ float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
802
+ List<LensInfo> logicalLenses = new ArrayList<>();
803
+ logicalLenses.add(new LensInfo(4.25f, "wideAngle", 1.0f, maxZoom));
804
+ devices.add(new com.ahm.capacitor.camera.preview.model.CameraDevice(
805
+ logicalCameraId, "Logical Camera (" + position + ")", position, logicalLenses, minZoom, maxZoom, true
806
+ ));
807
+ Log.d(TAG, "Found logical camera: " + logicalCameraId + " (" + position + ") with zoom " + minZoom + "-" + maxZoom);
808
+
809
+ // Get and add physical cameras
810
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
811
+ Set<CameraInfo> physicalCameraInfos = cameraInfo.getPhysicalCameraInfos();
812
+ if (physicalCameraInfos.isEmpty()) continue;
813
+
814
+ Log.d(TAG, "Logical camera " + logicalCameraId + " has " + physicalCameraInfos.size() + " physical cameras.");
815
+
816
+ for (CameraInfo physicalCameraInfo : physicalCameraInfos) {
817
+ String physicalId = Camera2CameraInfo.from(physicalCameraInfo).getCameraId();
818
+ if (physicalId.equals(logicalCameraId)) continue; // Already added as logical
819
+
820
+ try {
821
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(physicalId);
822
+ String deviceType = "wideAngle";
823
+ float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
824
+ android.util.SizeF sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
825
+
826
+ if (focalLengths != null && focalLengths.length > 0 && sensorSize != null && sensorSize.getWidth() > 0) {
827
+ double fov = 2 * Math.toDegrees(Math.atan(sensorSize.getWidth() / (2 * focalLengths[0])));
828
+ if (fov > 90) deviceType = "ultraWide";
829
+ else if (fov < 40) deviceType = "telephoto";
830
+ } else if (focalLengths != null && focalLengths.length > 0) {
831
+ if (focalLengths[0] < 3.0f) deviceType = "ultraWide";
832
+ else if (focalLengths[0] > 5.0f) deviceType = "telephoto";
833
+ }
834
+
835
+ float physicalMinZoom = 1.0f;
836
+ float physicalMaxZoom = 1.0f;
837
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
838
+ android.util.Range<Float> zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
839
+ if (zoomRange != null) {
840
+ physicalMinZoom = zoomRange.getLower();
841
+ physicalMaxZoom = zoomRange.getUpper();
842
+ }
843
+ }
844
+
845
+ String label = "Physical " + deviceType + " (" + position + ")";
846
+ List<LensInfo> physicalLenses = new ArrayList<>();
847
+ physicalLenses.add(new LensInfo(focalLengths != null ? focalLengths[0] : 4.25f, deviceType, 1.0f, physicalMaxZoom));
848
+
849
+ devices.add(new com.ahm.capacitor.camera.preview.model.CameraDevice(
850
+ physicalId, label, position, physicalLenses, physicalMinZoom, physicalMaxZoom, false
851
+ ));
852
+ Log.d(TAG, "Found physical camera: " + physicalId + " (" + label + ")");
853
+ } catch (CameraAccessException e) {
854
+ Log.e(TAG, "Failed to access characteristics for physical camera " + physicalId, e);
855
+ }
856
+ }
857
+ }
858
+ }
859
+ return devices;
860
+ } catch (Exception e) {
861
+ Log.e(TAG, "getAvailableDevicesStatic: Error getting devices", e);
862
+ return Collections.emptyList();
863
+ }
864
+ }
865
+
866
+ public static ZoomFactors getZoomFactorsStatic() {
867
+ try {
868
+ // For static method, return default zoom factors
869
+ // We can try to detect if ultra-wide is available by checking device list
870
+
871
+ float minZoom = 1.0f;
872
+ float maxZoom = 10.0f;
873
+
874
+ Log.d(TAG, "getZoomFactorsStatic: Final range - minZoom: " + minZoom + ", maxZoom: " + maxZoom);
875
+ LensInfo defaultLens = new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
876
+ return new ZoomFactors(minZoom, maxZoom, 1.0f, defaultLens);
877
+ } catch (Exception e) {
878
+ Log.e(TAG, "getZoomFactorsStatic: Error getting zoom factors", e);
879
+ LensInfo defaultLens = new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
880
+ return new ZoomFactors(1.0f, 10.0f, 1.0f, defaultLens);
881
+ }
882
+ }
883
+
884
+ public ZoomFactors getZoomFactors() {
885
+ if (camera == null) {
886
+ return getZoomFactorsStatic();
887
+ }
888
+
889
+ try {
890
+ // Get the current zoom from active camera
891
+ float currentZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
892
+ float minZoom = camera.getCameraInfo().getZoomState().getValue().getMinZoomRatio();
893
+ float maxZoom = camera.getCameraInfo().getZoomState().getValue().getMaxZoomRatio();
894
+
895
+ Log.d(TAG, "getZoomFactors: Combined range - minZoom: " + minZoom + ", maxZoom: " + maxZoom + ", currentZoom: " + currentZoom);
896
+
897
+ return new ZoomFactors(minZoom, maxZoom, currentZoom, getCurrentLensInfo());
898
+ } catch (Exception e) {
899
+ Log.e(TAG, "getZoomFactors: Error getting zoom factors", e);
900
+ return new ZoomFactors(1.0f, 1.0f, 1.0f, getCurrentLensInfo());
901
+ }
902
+ }
903
+
904
+
905
+
906
+ private LensInfo getCurrentLensInfo() {
907
+ if (camera == null) {
908
+ return new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
909
+ }
910
+
911
+ try {
912
+ float currentZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
913
+
914
+ // Determine device type based on zoom capabilities
915
+ String deviceType = "wideAngle";
916
+ float baseZoomRatio = 1.0f;
917
+
918
+ float digitalZoom = currentZoom / baseZoomRatio;
919
+
920
+ return new LensInfo(4.25f, deviceType, baseZoomRatio, digitalZoom);
921
+ } catch (Exception e) {
922
+ Log.e(TAG, "getCurrentLensInfo: Error getting lens info", e);
923
+ return new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
924
+ }
925
+ }
926
+
927
+ public void setZoom(float zoomRatio) throws Exception {
928
+ if (camera == null) {
929
+ throw new Exception("Camera not initialized");
930
+ }
931
+
932
+ Log.d(TAG, "setZoom: Requested zoom ratio: " + zoomRatio);
933
+
934
+ // Just let CameraX handle everything - it should automatically switch lenses
935
+ try {
936
+ ListenableFuture<Void> zoomFuture = camera.getCameraControl().setZoomRatio(zoomRatio);
937
+
938
+ // Add callback to see what actually happened
939
+ zoomFuture.addListener(() -> {
940
+ try {
941
+ float actualZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
942
+ Log.d(TAG, "setZoom: CameraX set zoom to " + actualZoom + " (requested: " + zoomRatio + ")");
943
+ if (Math.abs(actualZoom - zoomRatio) > 0.1f) {
944
+ Log.w(TAG, "setZoom: CameraX clamped zoom from " + zoomRatio + " to " + actualZoom);
945
+ } else {
946
+ Log.d(TAG, "setZoom: CameraX successfully set requested zoom");
947
+ }
948
+ } catch (Exception e) {
949
+ Log.e(TAG, "setZoom: Error checking final zoom", e);
950
+ }
951
+ }, mainExecutor);
952
+
953
+ } catch (Exception e) {
954
+ Log.e(TAG, "setZoom: Failed to set zoom to " + zoomRatio, e);
955
+ throw e;
956
+ }
957
+ }
958
+
959
+ public static List<Size> getSupportedPictureSizes(String facing) {
960
+ List<Size> sizes = new ArrayList<>();
961
+ try {
962
+ CameraSelector.Builder builder = new CameraSelector.Builder();
963
+ if ("front".equals(facing)) {
964
+ builder.requireLensFacing(CameraSelector.LENS_FACING_FRONT);
965
+ } else {
966
+ builder.requireLensFacing(CameraSelector.LENS_FACING_BACK);
967
+ }
968
+
969
+ // This part is complex because we need characteristics, which are not directly on CameraInfo.
970
+ // For now, returning a static list of common sizes.
971
+ // A more advanced implementation would use Camera2interop to get StreamConfigurationMap.
972
+ sizes.add(new Size(4032, 3024));
973
+ sizes.add(new Size(1920, 1080));
974
+ sizes.add(new Size(1280, 720));
975
+ sizes.add(new Size(640, 480));
976
+
977
+ } catch (Exception e) {
978
+ Log.e(TAG, "Error getting supported picture sizes", e);
979
+ }
980
+ return sizes;
981
+ }
982
+
983
+ private void setZoomInternal(float zoomRatio) {
984
+ if (camera != null) {
985
+ try {
986
+ float minZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getMinZoomRatio();
987
+ float maxZoom = camera.getCameraInfo().getZoomState().getValue().getMaxZoomRatio();
988
+ float currentZoom = camera.getCameraInfo().getZoomState().getValue().getZoomRatio();
989
+
990
+ Log.d(TAG, "setZoomInternal: Current camera range: " + minZoom + "-" + maxZoom + ", current: " + currentZoom);
991
+ Log.d(TAG, "setZoomInternal: Requesting zoom: " + zoomRatio);
992
+
993
+ // Try to set zoom directly - let CameraX handle lens switching
994
+ ListenableFuture<Void> zoomFuture = camera.getCameraControl().setZoomRatio(zoomRatio);
995
+
996
+ zoomFuture.addListener(() -> {
997
+ try {
998
+ zoomFuture.get(); // Check if zoom was successful
999
+ float newZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
1000
+ Log.d(TAG, "setZoomInternal: Zoom set successfully to " + newZoom + " (requested: " + zoomRatio + ")");
1001
+
1002
+ // Check if CameraX switched cameras
1003
+ String newCameraId = getCameraId(camera.getCameraInfo());
1004
+ if (!newCameraId.equals(currentDeviceId)) {
1005
+ currentDeviceId = newCameraId;
1006
+ Log.d(TAG, "setZoomInternal: CameraX switched to camera: " + newCameraId);
1007
+ }
1008
+ } catch (Exception e) {
1009
+ Log.w(TAG, "setZoomInternal: Zoom operation failed: " + e.getMessage());
1010
+ // Fallback: clamp to current camera's range
1011
+ float clampedZoom = Math.max(minZoom, Math.min(zoomRatio, maxZoom));
1012
+ camera.getCameraControl().setZoomRatio(clampedZoom);
1013
+ Log.d(TAG, "setZoomInternal: Fallback - clamped zoom to " + clampedZoom);
1014
+ }
1015
+ }, mainExecutor);
1016
+
1017
+ } catch (Exception e) {
1018
+ Log.e(TAG, "setZoomInternal: Error setting zoom", e);
1019
+ }
1020
+ }
1021
+ }
1022
+
1023
+ public static List<String> getSupportedFlashModesStatic() {
1024
+ try {
1025
+ // For static method, we can return common flash modes
1026
+ // Most modern cameras support these modes
1027
+ return Arrays.asList("off", "on", "auto");
1028
+ } catch (Exception e) {
1029
+ Log.e(TAG, "getSupportedFlashModesStatic: Error getting flash modes", e);
1030
+ return Collections.singletonList("off");
1031
+ }
1032
+ }
1033
+
1034
+ public List<String> getSupportedFlashModes() {
1035
+ if (camera == null) {
1036
+ return getSupportedFlashModesStatic();
1037
+ }
1038
+
1039
+ try {
1040
+ boolean hasFlash = camera.getCameraInfo().hasFlashUnit();
1041
+ if (hasFlash) {
1042
+ return Arrays.asList("off", "on", "auto");
1043
+ } else {
1044
+ return Collections.singletonList("off");
1045
+ }
1046
+ } catch (Exception e) {
1047
+ Log.e(TAG, "getSupportedFlashModes: Error getting flash modes", e);
1048
+ return Collections.singletonList("off");
1049
+ }
1050
+ }
1051
+
1052
+ public String getFlashMode() {
1053
+ switch (currentFlashMode) {
1054
+ case ImageCapture.FLASH_MODE_ON:
1055
+ return "on";
1056
+ case ImageCapture.FLASH_MODE_AUTO:
1057
+ return "auto";
1058
+ default:
1059
+ return "off";
1060
+ }
1061
+ }
1062
+
1063
+ public void setFlashMode(String mode) {
1064
+ int flashMode;
1065
+ switch (mode) {
1066
+ case "on":
1067
+ flashMode = ImageCapture.FLASH_MODE_ON;
1068
+ break;
1069
+ case "auto":
1070
+ flashMode = ImageCapture.FLASH_MODE_AUTO;
1071
+ break;
1072
+ default:
1073
+ flashMode = ImageCapture.FLASH_MODE_OFF;
1074
+ break;
1075
+ }
1076
+
1077
+ currentFlashMode = flashMode;
1078
+
1079
+ if (imageCapture != null) {
1080
+ imageCapture.setFlashMode(flashMode);
1081
+ }
1082
+ if (sampleImageCapture != null) {
1083
+ sampleImageCapture.setFlashMode(flashMode);
1084
+ }
1085
+ }
1086
+
1087
+ public String getCurrentDeviceId() {
1088
+ return currentDeviceId != null ? currentDeviceId : "unknown";
1089
+ }
1090
+
1091
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
1092
+ public void switchToDevice(String deviceId) {
1093
+ Log.d(TAG, "switchToDevice: Attempting to switch to device " + deviceId);
1094
+
1095
+ mainExecutor.execute(() -> {
1096
+ try {
1097
+ // Standard physical device selection logic...
1098
+ List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
1099
+ CameraInfo targetCameraInfo = null;
1100
+ for (CameraInfo cameraInfo : cameraInfos) {
1101
+ if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1102
+ targetCameraInfo = cameraInfo;
1103
+ break;
1104
+ }
1105
+ }
1106
+
1107
+ if (targetCameraInfo != null) {
1108
+ Log.d(TAG, "switchToDevice: Found matching CameraInfo for deviceId: " + deviceId);
1109
+ final CameraInfo finalTarget = targetCameraInfo;
1110
+
1111
+ // This filter will receive a list of all cameras and must return the one we want.
1112
+
1113
+ currentCameraSelector = new CameraSelector.Builder()
1114
+ .addCameraFilter(cameras -> {
1115
+ // This filter will receive a list of all cameras and must return the one we want.
1116
+ return Collections.singletonList(finalTarget);
1117
+ }).build();
1118
+ currentDeviceId = deviceId;
1119
+ bindCameraUseCases(); // Rebind with the new, highly specific selector
1120
+ } else {
1121
+ Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
1122
+ }
1123
+ } catch (Exception e) {
1124
+ Log.e(TAG, "switchToDevice: Error switching camera", e);
1125
+ }
1126
+ });
1127
+ }
1128
+
1129
+ public void flipCamera() {
1130
+ Log.d(TAG, "flipCamera: Flipping camera");
1131
+
1132
+ // Determine current position based on session config and flip it
1133
+ String currentPosition = sessionConfig.getPosition();
1134
+ String newPosition = "front".equals(currentPosition) ? "rear" : "front";
1135
+
1136
+ Log.d(TAG, "flipCamera: Switching from " + currentPosition + " to " + newPosition);
1137
+
1138
+ sessionConfig = new CameraSessionConfiguration(
1139
+ null, // deviceId - clear device ID to force position-based selection
1140
+ newPosition, // position
1141
+ sessionConfig.getX(), // x
1142
+ sessionConfig.getY(), // y
1143
+ sessionConfig.getWidth(), // width
1144
+ sessionConfig.getHeight(), // height
1145
+ sessionConfig.getPaddingBottom(), // paddingBottom
1146
+ sessionConfig.isToBack(), // toBack
1147
+ sessionConfig.isStoreToFile(), // storeToFile
1148
+ sessionConfig.isEnableOpacity(), // enableOpacity
1149
+ sessionConfig.isEnableZoom(), // enableZoom
1150
+ sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
1151
+ sessionConfig.isDisableAudio(), // disableAudio
1152
+ sessionConfig.getZoomFactor(), // zoomFactor
1153
+ sessionConfig.getAspectRatio(), // aspectRatio
1154
+ sessionConfig.getGridMode() // gridMode
1155
+ );
1156
+
1157
+ // Clear current device ID to force position-based selection
1158
+ currentDeviceId = null;
1159
+
1160
+ // Camera operations must run on main thread
1161
+ cameraExecutor.execute(() -> {
1162
+ currentCameraSelector = buildCameraSelector();
1163
+ bindCameraUseCases();
1164
+ });
1165
+ }
1166
+
1167
+ public void setOpacity(float opacity) {
1168
+ if (previewView != null) {
1169
+ previewView.setAlpha(opacity);
1170
+ }
1171
+ }
1172
+
1173
+ private void updateLayoutParams() {
1174
+ if (sessionConfig == null) return;
1175
+
1176
+ FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
1177
+ sessionConfig.getWidth(),
1178
+ sessionConfig.getHeight()
1179
+ );
1180
+ layoutParams.leftMargin = sessionConfig.getX();
1181
+ layoutParams.topMargin = sessionConfig.getY();
1182
+
1183
+ if (sessionConfig.getAspectRatio() != null) {
1184
+ String[] ratios = sessionConfig.getAspectRatio().split(":");
1185
+ // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1186
+ float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
1187
+ if (sessionConfig.getWidth() > 0) {
1188
+ layoutParams.height = (int) (sessionConfig.getWidth() / ratio);
1189
+ } else if (sessionConfig.getHeight() > 0) {
1190
+ layoutParams.width = (int) (sessionConfig.getHeight() * ratio);
1191
+ }
1192
+ }
1193
+
1194
+ previewView.setLayoutParams(layoutParams);
1195
+
1196
+ if (listener != null) {
1197
+ listener.onCameraStarted(
1198
+ sessionConfig.getWidth(),
1199
+ sessionConfig.getHeight(),
1200
+ sessionConfig.getX(),
1201
+ sessionConfig.getY()
1202
+ );
1203
+ }
1204
+ }
1205
+
1206
+ public String getAspectRatio() {
1207
+ if (sessionConfig != null) {
1208
+ return sessionConfig.getAspectRatio();
1209
+ }
1210
+ return "4:3";
1211
+ }
1212
+
1213
+ public String getGridMode() {
1214
+ if (sessionConfig != null) {
1215
+ return sessionConfig.getGridMode();
1216
+ }
1217
+ return "none";
1218
+ }
1219
+
1220
+ public void setAspectRatio(String aspectRatio) {
1221
+ setAspectRatio(aspectRatio, null, null);
1222
+ }
1223
+
1224
+ public void setAspectRatio(String aspectRatio, Float x, Float y) {
1225
+ setAspectRatio(aspectRatio, x, y, null);
1226
+ }
1227
+
1228
+ public void setAspectRatio(String aspectRatio, Float x, Float y, Runnable callback) {
1229
+ if (sessionConfig == null) {
1230
+ if (callback != null) callback.run();
1231
+ return;
1232
+ }
1233
+
1234
+ String currentAspectRatio = sessionConfig.getAspectRatio();
1235
+
1236
+ // Don't restart camera if aspect ratio hasn't changed and no position specified
1237
+ if (aspectRatio != null && aspectRatio.equals(currentAspectRatio) && x == null && y == null) {
1238
+ Log.d(TAG, "setAspectRatio: Aspect ratio " + aspectRatio + " is already set and no position specified, skipping");
1239
+ if (callback != null) callback.run();
1240
+ return;
1241
+ }
1242
+
1243
+ String currentGridMode = sessionConfig.getGridMode();
1244
+ Log.d(TAG, "setAspectRatio: Changing from " + currentAspectRatio + " to " + aspectRatio +
1245
+ (x != null && y != null ? " at position (" + x + ", " + y + ")" : " with auto-centering") +
1246
+ ", preserving grid mode: " + currentGridMode);
1247
+
1248
+ sessionConfig = new CameraSessionConfiguration(
1249
+ sessionConfig.getDeviceId(),
1250
+ sessionConfig.getPosition(),
1251
+ sessionConfig.getX(),
1252
+ sessionConfig.getY(),
1253
+ sessionConfig.getWidth(),
1254
+ sessionConfig.getHeight(),
1255
+ sessionConfig.getPaddingBottom(),
1256
+ sessionConfig.getToBack(),
1257
+ sessionConfig.getStoreToFile(),
1258
+ sessionConfig.getEnableOpacity(),
1259
+ sessionConfig.getEnableZoom(),
1260
+ sessionConfig.getDisableExifHeaderStripping(),
1261
+ sessionConfig.getDisableAudio(),
1262
+ sessionConfig.getZoomFactor(),
1263
+ aspectRatio,
1264
+ currentGridMode
1265
+ );
1266
+
1267
+ // Update layout and rebind camera with new aspect ratio
1268
+ if (isRunning && previewContainer != null) {
1269
+ mainExecutor.execute(() -> {
1270
+ // First update the UI layout
1271
+ updatePreviewLayoutForAspectRatio(aspectRatio, x, y);
1272
+
1273
+ // Then rebind the camera with new aspect ratio configuration
1274
+ Log.d(TAG, "setAspectRatio: Rebinding camera with new aspect ratio: " + aspectRatio);
1275
+ bindCameraUseCases();
1276
+
1277
+ // Preserve grid mode and wait for completion
1278
+ if (gridOverlayView != null) {
1279
+ gridOverlayView.post(() -> {
1280
+ Log.d(TAG, "setAspectRatio: Re-applying grid mode: " + currentGridMode);
1281
+ gridOverlayView.setGridMode(currentGridMode);
1282
+
1283
+ // Wait one more frame for grid to be applied, then call callback
1284
+ if (callback != null) {
1285
+ gridOverlayView.post(callback);
1286
+ }
1287
+ });
1288
+ } else {
1289
+ // No grid overlay, wait one frame for layout completion then call callback
1290
+ if (callback != null) {
1291
+ previewContainer.post(callback);
1292
+ }
1293
+ }
1294
+ });
1295
+ } else {
1296
+ if (callback != null) callback.run();
1297
+ }
1298
+ }
1299
+
1300
+ public void setGridMode(String gridMode) {
1301
+ if (sessionConfig != null) {
1302
+ Log.d(TAG, "setGridMode: Changing grid mode to: " + gridMode);
1303
+ sessionConfig = new CameraSessionConfiguration(
1304
+ sessionConfig.getDeviceId(),
1305
+ sessionConfig.getPosition(),
1306
+ sessionConfig.getX(),
1307
+ sessionConfig.getY(),
1308
+ sessionConfig.getWidth(),
1309
+ sessionConfig.getHeight(),
1310
+ sessionConfig.getPaddingBottom(),
1311
+ sessionConfig.getToBack(),
1312
+ sessionConfig.getStoreToFile(),
1313
+ sessionConfig.getEnableOpacity(),
1314
+ sessionConfig.getEnableZoom(),
1315
+ sessionConfig.getDisableExifHeaderStripping(),
1316
+ sessionConfig.getDisableAudio(),
1317
+ sessionConfig.getZoomFactor(),
1318
+ sessionConfig.getAspectRatio(),
1319
+ gridMode
1320
+ );
1321
+
1322
+ // Update the grid overlay immediately
1323
+ if (gridOverlayView != null) {
1324
+ gridOverlayView.post(() -> {
1325
+ Log.d(TAG, "setGridMode: Applying grid mode to overlay: " + gridMode);
1326
+ gridOverlayView.setGridMode(gridMode);
1327
+ });
1328
+ }
1329
+ }
1330
+ }
1331
+
1332
+ public int getPreviewX() {
1333
+ if (previewContainer == null) return 0;
1334
+ ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
1335
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1336
+ // Return position relative to WebView content (subtract insets)
1337
+ int margin = ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
1338
+ int leftInset = getWebViewLeftInset();
1339
+ int result = margin - leftInset;
1340
+ Log.d(TAG, "getPreviewX: leftMargin=" + margin + ", leftInset=" + leftInset + ", result=" + result);
1341
+ return result;
1342
+ }
1343
+ return previewContainer.getLeft();
1344
+ }
1345
+ public int getPreviewY() {
1346
+ if (previewContainer == null) return 0;
1347
+ ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
1348
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1349
+ // Return position relative to WebView content (subtract insets)
1350
+ int margin = ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
1351
+ int topInset = getWebViewTopInset();
1352
+ int result = margin - topInset;
1353
+ Log.d(TAG, "getPreviewY: topMargin=" + margin + ", topInset=" + topInset + ", result=" + result);
1354
+ return result;
1355
+ }
1356
+ return previewContainer.getTop();
1357
+ }
1358
+ public int getPreviewWidth() {
1359
+ return previewContainer != null ? previewContainer.getWidth() : 0;
1360
+ }
1361
+ public int getPreviewHeight() {
1362
+ return previewContainer != null ? previewContainer.getHeight() : 0;
1363
+ }
1364
+ public void setPreviewSize(int x, int y, int width, int height) {
1365
+ setPreviewSize(x, y, width, height, null);
1366
+ }
1367
+
1368
+ public void setPreviewSize(int x, int y, int width, int height, Runnable callback) {
1369
+ if (previewContainer == null) {
1370
+ if (callback != null) callback.run();
1371
+ return;
1372
+ }
1373
+
1374
+ // Ensure this runs on the main UI thread
1375
+ mainExecutor.execute(() -> {
1376
+ ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
1377
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1378
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) layoutParams;
1379
+
1380
+ // Only add insets for positioning coordinates, not for full-screen sizes
1381
+ int webViewTopInset = getWebViewTopInset();
1382
+ int webViewLeftInset = getWebViewLeftInset();
1383
+
1384
+ // Handle positioning - preserve current values if new values are not specified (negative)
1385
+ if (x >= 0) {
1386
+ // Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
1387
+ if (x == 0 && y == 0) {
1388
+ params.leftMargin = x;
1389
+ Log.d(TAG, "setPreviewSize: Full-screen mode - keeping x=0 without insets");
1390
+ } else {
1391
+ params.leftMargin = x + webViewLeftInset;
1392
+ Log.d(TAG, "setPreviewSize: Positioned mode - x=" + x + " + inset=" + webViewLeftInset + " = " + (x + webViewLeftInset));
1393
+ }
1394
+ }
1395
+ if (y >= 0) {
1396
+ // Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
1397
+ if (x == 0 && y == 0) {
1398
+ params.topMargin = y;
1399
+ Log.d(TAG, "setPreviewSize: Full-screen mode - keeping y=0 without insets");
1400
+ } else {
1401
+ params.topMargin = y + webViewTopInset;
1402
+ Log.d(TAG, "setPreviewSize: Positioned mode - y=" + y + " + inset=" + webViewTopInset + " = " + (y + webViewTopInset));
1403
+ }
1404
+ }
1405
+ if (width > 0) params.width = width;
1406
+ if (height > 0) params.height = height;
1407
+
1408
+ previewContainer.setLayoutParams(params);
1409
+ previewContainer.requestLayout();
1410
+
1411
+ Log.d(TAG, "setPreviewSize: Updated to " + params.width + "x" + params.height + " at (" + params.leftMargin + "," + params.topMargin + ")");
1412
+
1413
+ // Update session config to reflect actual layout
1414
+ if (sessionConfig != null) {
1415
+ String currentAspectRatio = sessionConfig.getAspectRatio();
1416
+
1417
+ // Calculate aspect ratio from actual dimensions if both width and height are provided
1418
+ String calculatedAspectRatio = currentAspectRatio;
1419
+ if (params.width > 0 && params.height > 0) {
1420
+ // Always use larger dimension / smaller dimension for consistent comparison
1421
+ float ratio = Math.max(params.width, params.height) / (float) Math.min(params.width, params.height);
1422
+ // Standard ratios: 16:9 ≈ 1.778, 4:3 ≈ 1.333
1423
+ float ratio16_9 = 16f / 9f; // 1.778
1424
+ float ratio4_3 = 4f / 3f; // 1.333
1425
+
1426
+ // Determine closest standard aspect ratio
1427
+ if (Math.abs(ratio - ratio16_9) < Math.abs(ratio - ratio4_3)) {
1428
+ calculatedAspectRatio = "16:9";
1429
+ } else {
1430
+ calculatedAspectRatio = "4:3";
1431
+ }
1432
+ Log.d(TAG, "setPreviewSize: Calculated aspect ratio from " + params.width + "x" + params.height + " = " + calculatedAspectRatio + " (normalized ratio=" + ratio + ")");
1433
+ }
1434
+
1435
+ sessionConfig = new CameraSessionConfiguration(
1436
+ sessionConfig.getDeviceId(),
1437
+ sessionConfig.getPosition(),
1438
+ params.leftMargin,
1439
+ params.topMargin,
1440
+ params.width,
1441
+ params.height,
1442
+ sessionConfig.getPaddingBottom(),
1443
+ sessionConfig.getToBack(),
1444
+ sessionConfig.getStoreToFile(),
1445
+ sessionConfig.getEnableOpacity(),
1446
+ sessionConfig.getEnableZoom(),
1447
+ sessionConfig.getDisableExifHeaderStripping(),
1448
+ sessionConfig.getDisableAudio(),
1449
+ sessionConfig.getZoomFactor(),
1450
+ calculatedAspectRatio,
1451
+ sessionConfig.getGridMode()
1452
+ );
1453
+
1454
+ // If aspect ratio changed due to size update, rebind camera
1455
+ if (isRunning && !Objects.equals(currentAspectRatio, calculatedAspectRatio)) {
1456
+ Log.d(TAG, "setPreviewSize: Aspect ratio changed from " + currentAspectRatio + " to " + calculatedAspectRatio + ", rebinding camera");
1457
+ bindCameraUseCases();
1458
+
1459
+ // Wait for camera rebinding to complete, then call callback
1460
+ if (callback != null) {
1461
+ previewContainer.post(() -> previewContainer.post(callback));
1462
+ }
1463
+ } else {
1464
+ // No camera rebinding needed, wait for layout to complete then call callback
1465
+ if (callback != null) {
1466
+ previewContainer.post(callback);
1467
+ }
1468
+ }
1469
+ } else {
1470
+ // No sessionConfig, just wait for layout then call callback
1471
+ if (callback != null) {
1472
+ previewContainer.post(callback);
1473
+ }
1474
+ }
1475
+ } else {
1476
+ Log.w(TAG, "setPreviewSize: Cannot set margins on layout params of type " + layoutParams.getClass().getSimpleName());
1477
+ // Fallback: just set width and height if specified
1478
+ if (width > 0) layoutParams.width = width;
1479
+ if (height > 0) layoutParams.height = height;
1480
+ previewContainer.setLayoutParams(layoutParams);
1481
+ previewContainer.requestLayout();
1482
+
1483
+ // Wait for layout then call callback
1484
+ if (callback != null) {
1485
+ previewContainer.post(callback);
1486
+ }
1487
+ }
1488
+ });
1489
+ }
1490
+
1491
+ private void updatePreviewLayoutForAspectRatio(String aspectRatio) {
1492
+ updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
1493
+ }
1494
+
1495
+ private void updatePreviewLayoutForAspectRatio(String aspectRatio, Float x, Float y) {
1496
+ if (previewContainer == null || aspectRatio == null) return;
1497
+
1498
+ // Parse aspect ratio
1499
+ String[] ratios = aspectRatio.split(":");
1500
+ if (ratios.length != 2) return;
1501
+
1502
+ try {
1503
+ // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1504
+ float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
1505
+
1506
+ // Get available space from webview dimensions
1507
+ int availableWidth = webView.getWidth();
1508
+ int availableHeight = webView.getHeight();
1509
+
1510
+ // Calculate position and size
1511
+ int finalX, finalY, finalWidth, finalHeight;
1512
+
1513
+ if (x != null && y != null) {
1514
+ // Account for WebView insets from edge-to-edge support
1515
+ int webViewTopInset = getWebViewTopInset();
1516
+ int webViewLeftInset = getWebViewLeftInset();
1517
+
1518
+ // Use provided coordinates with boundary checking, adjusted for insets
1519
+ finalX = Math.max(0, Math.min(x.intValue() + webViewLeftInset, availableWidth));
1520
+ finalY = Math.max(0, Math.min(y.intValue() + webViewTopInset, availableHeight));
1521
+
1522
+ // Calculate maximum available space from the given position
1523
+ int maxWidth = availableWidth - finalX;
1524
+ int maxHeight = availableHeight - finalY;
1525
+
1526
+ // Calculate optimal size while maintaining aspect ratio within available space
1527
+ finalWidth = maxWidth;
1528
+ finalHeight = (int) (maxWidth / ratio);
1529
+
1530
+ if (finalHeight > maxHeight) {
1531
+ // Height constraint is tighter, fit by height
1532
+ finalHeight = maxHeight;
1533
+ finalWidth = (int) (maxHeight * ratio);
1534
+ }
1535
+
1536
+ // Ensure final position stays within bounds
1537
+ finalX = Math.max(0, Math.min(finalX, availableWidth - finalWidth));
1538
+ finalY = Math.max(0, Math.min(finalY, availableHeight - finalHeight));
1539
+ } else {
1540
+ // Auto-center the view
1541
+ // Calculate size based on aspect ratio, using a reasonable base size
1542
+ // Use 80% of available space to ensure aspect ratio differences are visible
1543
+ int maxAvailableWidth = (int) (availableWidth * 0.8);
1544
+ int maxAvailableHeight = (int) (availableHeight * 0.8);
1545
+
1546
+ // Start with width-based calculation
1547
+ finalWidth = maxAvailableWidth;
1548
+ finalHeight = (int) (finalWidth / ratio);
1549
+
1550
+ // If height exceeds available space, use height-based calculation
1551
+ if (finalHeight > maxAvailableHeight) {
1552
+ finalHeight = maxAvailableHeight;
1553
+ finalWidth = (int) (finalHeight * ratio);
1554
+ }
1555
+
1556
+ // Center the view
1557
+ finalX = (availableWidth - finalWidth) / 2;
1558
+ finalY = (availableHeight - finalHeight) / 2;
1559
+
1560
+ Log.d(TAG, "updatePreviewLayoutForAspectRatio: Auto-center mode - ratio=" + ratio +
1561
+ ", calculated size=" + finalWidth + "x" + finalHeight +
1562
+ ", available=" + availableWidth + "x" + availableHeight);
1563
+ }
1564
+
1565
+ // Update layout params
1566
+ ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
1567
+ if (currentParams instanceof ViewGroup.MarginLayoutParams) {
1568
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) currentParams;
1569
+ params.width = finalWidth;
1570
+ params.height = finalHeight;
1571
+ params.leftMargin = finalX;
1572
+ params.topMargin = finalY;
1573
+ previewContainer.setLayoutParams(params);
1574
+ previewContainer.requestLayout();
1575
+ Log.d(TAG, "updatePreviewLayoutForAspectRatio: Updated to " + finalWidth + "x" + finalHeight + " at (" + finalX + "," + finalY + ")");
1576
+ }
1577
+ } catch (NumberFormatException e) {
1578
+ Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
1579
+ }
1580
+ }
1581
+
1582
+ private void updatePreviewLayout() {
1583
+ if (previewContainer == null || sessionConfig == null) return;
1584
+
1585
+ String aspectRatio = sessionConfig.getAspectRatio();
1586
+ if (aspectRatio == null) return;
1587
+
1588
+ updatePreviewLayoutForAspectRatio(aspectRatio);
1589
+ }
1590
+
1591
+ private int getWebViewTopInset() {
1592
+ try {
1593
+ if (webView != null) {
1594
+ ViewGroup.LayoutParams layoutParams = webView.getLayoutParams();
1595
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1596
+ return ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
1597
+ }
1598
+ }
1599
+ } catch (Exception e) {
1600
+ Log.w(TAG, "Failed to get WebView top inset", e);
1601
+ }
1602
+ return 0;
1603
+ }
1604
+
1605
+ private int getWebViewLeftInset() {
1606
+ try {
1607
+ if (webView != null) {
1608
+ ViewGroup.LayoutParams layoutParams = webView.getLayoutParams();
1609
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1610
+ return ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
1611
+ }
1612
+ }
1613
+ } catch (Exception e) {
1614
+ Log.w(TAG, "Failed to get WebView left inset", e);
1615
+ }
1616
+ return 0;
1617
+ }
1618
+
1619
+ /**
1620
+ * Get the current preview position and size in DP units (without insets)
1621
+ */
1622
+ public int[] getCurrentPreviewBounds() {
1623
+ if (previewContainer == null) {
1624
+ return new int[]{0, 0, 0, 0}; // x, y, width, height
1625
+ }
1626
+
1627
+ ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
1628
+ int x = 0, y = 0, width = 0, height = 0;
1629
+
1630
+ if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
1631
+ ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) layoutParams;
1632
+
1633
+ // Remove insets to get original coordinates in DP
1634
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
1635
+ float pixelRatio = metrics.density;
1636
+
1637
+ int webViewTopInset = getWebViewTopInset();
1638
+ int webViewLeftInset = getWebViewLeftInset();
1639
+
1640
+ x = Math.max(0, (int) ((params.leftMargin - webViewLeftInset) / pixelRatio));
1641
+ y = Math.max(0, (int) ((params.topMargin - webViewTopInset) / pixelRatio));
1642
+ width = (int) (params.width / pixelRatio);
1643
+ height = (int) (params.height / pixelRatio);
1644
+ }
1645
+
1646
+ return new int[]{x, y, width, height};
1647
+ }
1648
+ }