@capgo/camera-preview 7.4.0-beta.1 → 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 (31) hide show
  1. package/README.md +195 -31
  2. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
  4. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  5. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  6. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  7. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  8. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  9. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  10. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  11. package/android/.gradle/file-system.probe +0 -0
  12. package/android/build.gradle +3 -1
  13. package/android/src/main/AndroidManifest.xml +5 -3
  14. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +282 -45
  15. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +902 -102
  16. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +82 -0
  17. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +19 -5
  18. package/dist/docs.json +235 -6
  19. package/dist/esm/definitions.d.ts +119 -3
  20. package/dist/esm/definitions.js.map +1 -1
  21. package/dist/esm/web.d.ts +47 -3
  22. package/dist/esm/web.js +262 -78
  23. package/dist/esm/web.js.map +1 -1
  24. package/dist/plugin.cjs.js +258 -78
  25. package/dist/plugin.cjs.js.map +1 -1
  26. package/dist/plugin.js +258 -78
  27. package/dist/plugin.js.map +1 -1
  28. package/ios/Sources/CapgoCameraPreview/CameraController.swift +245 -28
  29. package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
  30. package/ios/Sources/CapgoCameraPreview/Plugin.swift +657 -90
  31. package/package.json +1 -1
@@ -4,8 +4,8 @@ import android.content.Context;
4
4
  import android.hardware.camera2.CameraAccessException;
5
5
  import android.hardware.camera2.CameraManager;
6
6
  import android.os.Build;
7
- import android.os.HandlerThread;
8
7
  import android.util.Base64;
8
+ import android.util.DisplayMetrics;
9
9
  import android.util.Log;
10
10
  import android.util.Size;
11
11
  import android.view.ViewGroup;
@@ -19,6 +19,8 @@ import androidx.camera.core.ImageCapture;
19
19
  import androidx.camera.core.ImageCaptureException;
20
20
  import androidx.camera.core.ImageProxy;
21
21
  import androidx.camera.core.Preview;
22
+ import androidx.camera.core.AspectRatio;
23
+ import androidx.camera.core.resolutionselector.AspectRatioStrategy;
22
24
  import androidx.camera.core.resolutionselector.ResolutionSelector;
23
25
  import androidx.camera.core.resolutionselector.ResolutionStrategy;
24
26
  import androidx.camera.lifecycle.ProcessCameraProvider;
@@ -28,37 +30,56 @@ import androidx.lifecycle.Lifecycle;
28
30
  import androidx.lifecycle.LifecycleOwner;
29
31
  import androidx.lifecycle.LifecycleRegistry;
30
32
  import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
33
+ import android.widget.FrameLayout;
31
34
  import com.ahm.capacitor.camera.preview.model.LensInfo;
32
35
  import com.ahm.capacitor.camera.preview.model.ZoomFactors;
33
36
  import com.google.common.util.concurrent.ListenableFuture;
34
37
  import java.nio.ByteBuffer;
35
- import java.nio.file.Files;
36
38
  import java.util.Arrays;
37
39
  import java.util.Collections;
38
40
  import java.util.List;
39
41
  import java.util.ArrayList;
40
42
  import java.util.Objects;
41
- import java.util.concurrent.ExecutionException;
42
43
  import java.util.concurrent.Executor;
43
44
  import java.util.concurrent.ExecutorService;
44
45
  import java.util.concurrent.Executors;
45
46
  import androidx.camera.camera2.interop.Camera2CameraInfo;
46
47
  import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
47
48
  import android.hardware.camera2.CameraCharacteristics;
48
- import androidx.camera.extensions.ExtensionMode;
49
49
  import java.util.Set;
50
50
  import androidx.camera.core.ZoomState;
51
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;
52
73
 
53
- public class CameraXView implements LifecycleOwner {
74
+ public class CameraXView implements LifecycleOwner, LifecycleObserver {
54
75
  private static final String TAG = "CameraPreview CameraXView";
55
76
 
56
77
  public interface CameraXViewListener {
57
- void onPictureTaken(String result);
78
+ void onPictureTaken(String base64, JSONObject exif);
58
79
  void onPictureTakenError(String message);
59
80
  void onSampleTaken(String result);
60
81
  void onSampleTakenError(String message);
61
- void onCameraStarted();
82
+ void onCameraStarted(int width, int height, int x, int y);
62
83
  void onCameraStartError(String message);
63
84
  }
64
85
 
@@ -67,6 +88,8 @@ public class CameraXView implements LifecycleOwner {
67
88
  private ImageCapture imageCapture;
68
89
  private ImageCapture sampleImageCapture;
69
90
  private PreviewView previewView;
91
+ private GridOverlayView gridOverlayView;
92
+ private FrameLayout previewContainer;
70
93
  private CameraSelector currentCameraSelector;
71
94
  private String currentDeviceId;
72
95
  private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
@@ -102,6 +125,26 @@ public class CameraXView implements LifecycleOwner {
102
125
  return isRunning;
103
126
  }
104
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
+
105
148
  public void startSession(CameraSessionConfiguration config) {
106
149
  this.sessionConfig = config;
107
150
  cameraExecutor = Executors.newSingleThreadExecutor();
@@ -147,35 +190,140 @@ public class CameraXView implements LifecycleOwner {
147
190
  if (sessionConfig.isToBack()) {
148
191
  webView.setBackgroundColor(android.graphics.Color.TRANSPARENT);
149
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
150
198
  previewView = new PreviewView(context);
151
- previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
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
+
152
218
  ViewGroup parent = (ViewGroup) webView.getParent();
153
219
  if (parent != null) {
154
- parent.addView(previewView, new ViewGroup.LayoutParams(sessionConfig.getWidth(), sessionConfig.getHeight()));
220
+ FrameLayout.LayoutParams layoutParams = calculatePreviewLayoutParams();
221
+ parent.addView(previewContainer, layoutParams);
155
222
  if(sessionConfig.isToBack()) webView.bringToFront();
156
223
  }
157
224
  }
158
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
+
159
287
  private void removePreviewView() {
160
- if (previewView != null) {
161
- ViewGroup parent = (ViewGroup) previewView.getParent();
288
+ if (previewContainer != null) {
289
+ ViewGroup parent = (ViewGroup) previewContainer.getParent();
162
290
  if (parent != null) {
163
- parent.removeView(previewView);
291
+ parent.removeView(previewContainer);
164
292
  }
293
+ previewContainer = null;
294
+ }
295
+ if (previewView != null) {
165
296
  previewView = null;
166
297
  }
298
+ if (gridOverlayView != null) {
299
+ gridOverlayView = null;
300
+ }
167
301
  webView.setBackgroundColor(android.graphics.Color.WHITE);
168
302
  }
169
303
 
304
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
170
305
  private void bindCameraUseCases() {
171
306
  if (cameraProvider == null) return;
172
307
  mainExecutor.execute(() -> {
173
308
  try {
174
309
  Log.d(TAG, "Building camera selector with deviceId: " + sessionConfig.getDeviceId() + " and position: " + sessionConfig.getPosition());
175
310
  currentCameraSelector = buildCameraSelector();
176
- ResolutionSelector resolutionSelector = new ResolutionSelector.Builder()
177
- .setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)
178
- .build();
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
+
179
327
  Preview preview = new Preview.Builder().setResolutionSelector(resolutionSelector).build();
180
328
  imageCapture = new ImageCapture.Builder().setResolutionSelector(resolutionSelector).setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).setFlashMode(currentFlashMode).build();
181
329
  sampleImageCapture = imageCapture;
@@ -188,7 +336,6 @@ public class CameraXView implements LifecycleOwner {
188
336
  Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
189
337
  CameraInfo cameraInfo = camera.getCameraInfo();
190
338
  Log.d(TAG, "Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId());
191
- Log.d(TAG, "Implementation Type: " + cameraInfo.getImplementationType());
192
339
 
193
340
  // Log zoom state
194
341
  ZoomState zoomState = cameraInfo.getZoomState().getValue();
@@ -227,7 +374,17 @@ public class CameraXView implements LifecycleOwner {
227
374
 
228
375
  isRunning = true;
229
376
  Log.d(TAG, "bindCameraUseCases: Camera bound successfully");
230
- if (listener != null) listener.onCameraStarted();
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
+ }
231
388
  } catch (Exception e) {
232
389
  if (listener != null) listener.onCameraStartError("Error binding camera: " + e.getMessage());
233
390
  }
@@ -256,22 +413,6 @@ public class CameraXView implements LifecycleOwner {
256
413
  return builder.build();
257
414
  }
258
415
 
259
- private static boolean isIsLogical(CameraManager cameraManager, String cameraId) throws CameraAccessException {
260
- CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
261
- int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
262
-
263
- boolean isLogical = false;
264
- if (capabilities != null) {
265
- for (int capability : capabilities) {
266
- if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) {
267
- isLogical = true;
268
- break;
269
- }
270
- }
271
- }
272
- return isLogical;
273
- }
274
-
275
416
  private static String getCameraId(androidx.camera.core.CameraInfo cameraInfo) {
276
417
  try {
277
418
  // Generate a stable ID based on camera characteristics
@@ -303,7 +444,7 @@ public class CameraXView implements LifecycleOwner {
303
444
  }
304
445
  }
305
446
 
306
- public void capturePhoto(int quality) {
447
+ public void capturePhoto(int quality, final boolean saveToGallery, Integer width, Integer height, Location location) {
307
448
  Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality);
308
449
 
309
450
  if (imageCapture == null) {
@@ -313,9 +454,8 @@ public class CameraXView implements LifecycleOwner {
313
454
  return;
314
455
  }
315
456
 
316
- ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(
317
- new java.io.File(context.getCacheDir(), "temp_image.jpg")
318
- ).build();
457
+ File tempFile = new File(context.getCacheDir(), "temp_image.jpg");
458
+ ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(tempFile).build();
319
459
 
320
460
  imageCapture.takePicture(
321
461
  outputFileOptions,
@@ -331,31 +471,52 @@ public class CameraXView implements LifecycleOwner {
331
471
 
332
472
  @Override
333
473
  public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
334
- // Convert to base64
335
474
  try {
336
- java.io.File tempFile = new java.io.File(context.getCacheDir(), "temp_image.jpg");
337
- byte[] bytes;
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
+ }
338
486
 
339
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
340
- bytes = Files.readAllBytes(tempFile.toPath());
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);
341
498
  } else {
342
- // Fallback for older Android versions
343
- java.io.FileInputStream fis = new java.io.FileInputStream(tempFile);
499
+ // For non-resized images, ensure EXIF is saved
500
+ exifInterface.saveAttributes();
344
501
  bytes = new byte[(int) tempFile.length()];
345
- fis.read(bytes);
346
- fis.close();
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);
347
509
  }
348
510
 
349
511
  String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
350
512
 
351
- // Clean up temp file
352
513
  tempFile.delete();
353
514
 
354
515
  if (listener != null) {
355
- listener.onPictureTaken(base64);
516
+ listener.onPictureTaken(base64, exifData);
356
517
  }
357
518
  } catch (Exception e) {
358
- Log.e(TAG, "capturePhoto: Error converting to base64", e);
519
+ Log.e(TAG, "capturePhoto: Error processing image", e);
359
520
  if (listener != null) {
360
521
  listener.onPictureTakenError("Error processing image: " + e.getMessage());
361
522
  }
@@ -365,6 +526,210 @@ public class CameraXView implements LifecycleOwner {
365
526
  );
366
527
  }
367
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
+
368
733
  public void captureSample(int quality) {
369
734
  Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality);
370
735
 
@@ -430,7 +795,7 @@ public class CameraXView implements LifecycleOwner {
430
795
  for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
431
796
  String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
432
797
  String position = isBackCamera(cameraInfo) ? "rear" : "front";
433
-
798
+
434
799
  // Add logical camera
435
800
  float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
436
801
  float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
@@ -466,7 +831,7 @@ public class CameraXView implements LifecycleOwner {
466
831
  if (focalLengths[0] < 3.0f) deviceType = "ultraWide";
467
832
  else if (focalLengths[0] > 5.0f) deviceType = "telephoto";
468
833
  }
469
-
834
+
470
835
  float physicalMinZoom = 1.0f;
471
836
  float physicalMaxZoom = 1.0f;
472
837
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
@@ -476,11 +841,11 @@ public class CameraXView implements LifecycleOwner {
476
841
  physicalMaxZoom = zoomRange.getUpper();
477
842
  }
478
843
  }
479
-
844
+
480
845
  String label = "Physical " + deviceType + " (" + position + ")";
481
846
  List<LensInfo> physicalLenses = new ArrayList<>();
482
847
  physicalLenses.add(new LensInfo(focalLengths != null ? focalLengths[0] : 4.25f, deviceType, 1.0f, physicalMaxZoom));
483
-
848
+
484
849
  devices.add(new com.ahm.capacitor.camera.preview.model.CameraDevice(
485
850
  physicalId, label, position, physicalLenses, physicalMinZoom, physicalMaxZoom, false
486
851
  ));
@@ -498,11 +863,10 @@ public class CameraXView implements LifecycleOwner {
498
863
  }
499
864
  }
500
865
 
501
- public static ZoomFactors getZoomFactorsStatic(Context context) {
866
+ public static ZoomFactors getZoomFactorsStatic() {
502
867
  try {
503
868
  // For static method, return default zoom factors
504
869
  // We can try to detect if ultra-wide is available by checking device list
505
- List<com.ahm.capacitor.camera.preview.model.CameraDevice> devices = getAvailableDevicesStatic(context);
506
870
 
507
871
  float minZoom = 1.0f;
508
872
  float maxZoom = 10.0f;
@@ -519,7 +883,7 @@ public class CameraXView implements LifecycleOwner {
519
883
 
520
884
  public ZoomFactors getZoomFactors() {
521
885
  if (camera == null) {
522
- return getZoomFactorsStatic(context);
886
+ return getZoomFactorsStatic();
523
887
  }
524
888
 
525
889
  try {
@@ -546,8 +910,6 @@ public class CameraXView implements LifecycleOwner {
546
910
 
547
911
  try {
548
912
  float currentZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
549
- float minZoom = camera.getCameraInfo().getZoomState().getValue().getMinZoomRatio();
550
- float maxZoom = camera.getCameraInfo().getZoomState().getValue().getMaxZoomRatio();
551
913
 
552
914
  // Determine device type based on zoom capabilities
553
915
  String deviceType = "wideAngle";
@@ -594,46 +956,6 @@ public class CameraXView implements LifecycleOwner {
594
956
  }
595
957
  }
596
958
 
597
- private List<androidx.camera.core.CameraInfo> getAvailableCamerasForCurrentPosition() {
598
- if (cameraProvider == null) {
599
- Log.w(TAG, "getAvailableCamerasForCurrentPosition: cameraProvider is null");
600
- return Collections.emptyList();
601
- }
602
-
603
- List<androidx.camera.core.CameraInfo> allCameras = cameraProvider.getAvailableCameraInfos();
604
- List<androidx.camera.core.CameraInfo> sameFacingCameras = new ArrayList<>();
605
-
606
- Log.d(TAG, "getAvailableCamerasForCurrentPosition: Total cameras available: " + allCameras.size());
607
-
608
- // Determine current facing direction from the session config to avoid restricted API call
609
- boolean isCurrentBack = "back".equals(sessionConfig.getPosition());
610
- Log.d(TAG, "getAvailableCamerasForCurrentPosition: Looking for " + (isCurrentBack ? "back" : "front") + " cameras");
611
-
612
- for (int i = 0; i < allCameras.size(); i++) {
613
- androidx.camera.core.CameraInfo cameraInfo = allCameras.get(i);
614
- boolean isCameraBack = isBackCamera(cameraInfo);
615
- String cameraId = getCameraId(cameraInfo);
616
-
617
- Log.d(TAG, "getAvailableCamerasForCurrentPosition: Camera " + i + " - ID: " + cameraId + ", isBack: " + isCameraBack);
618
-
619
- try {
620
- float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
621
- float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
622
- Log.d(TAG, "getAvailableCamerasForCurrentPosition: Camera " + i + " zoom range: " + minZoom + "-" + maxZoom);
623
- } catch (Exception e) {
624
- Log.w(TAG, "getAvailableCamerasForCurrentPosition: Cannot get zoom info for camera " + i + ": " + e.getMessage());
625
- }
626
-
627
- if (isCameraBack == isCurrentBack) {
628
- sameFacingCameras.add(cameraInfo);
629
- Log.d(TAG, "getAvailableCamerasForCurrentPosition: Added camera " + i + " (" + cameraId + ") to same-facing list");
630
- }
631
- }
632
-
633
- Log.d(TAG, "getAvailableCamerasForCurrentPosition: Found " + sameFacingCameras.size() + " cameras for " + (isCurrentBack ? "back" : "front"));
634
- return sameFacingCameras;
635
- }
636
-
637
959
  public static List<Size> getSupportedPictureSizes(String facing) {
638
960
  List<Size> sizes = new ArrayList<>();
639
961
  try {
@@ -786,13 +1108,13 @@ public class CameraXView implements LifecycleOwner {
786
1108
  Log.d(TAG, "switchToDevice: Found matching CameraInfo for deviceId: " + deviceId);
787
1109
  final CameraInfo finalTarget = targetCameraInfo;
788
1110
 
789
- CameraSelector newSelector = new CameraSelector.Builder()
1111
+ // This filter will receive a list of all cameras and must return the one we want.
1112
+
1113
+ currentCameraSelector = new CameraSelector.Builder()
790
1114
  .addCameraFilter(cameras -> {
791
1115
  // This filter will receive a list of all cameras and must return the one we want.
792
1116
  return Collections.singletonList(finalTarget);
793
1117
  }).build();
794
-
795
- currentCameraSelector = newSelector;
796
1118
  currentDeviceId = deviceId;
797
1119
  bindCameraUseCases(); // Rebind with the new, highly specific selector
798
1120
  } else {
@@ -827,7 +1149,9 @@ public class CameraXView implements LifecycleOwner {
827
1149
  sessionConfig.isEnableZoom(), // enableZoom
828
1150
  sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
829
1151
  sessionConfig.isDisableAudio(), // disableAudio
830
- sessionConfig.getZoomFactor() // zoomFactor
1152
+ sessionConfig.getZoomFactor(), // zoomFactor
1153
+ sessionConfig.getAspectRatio(), // aspectRatio
1154
+ sessionConfig.getGridMode() // gridMode
831
1155
  );
832
1156
 
833
1157
  // Clear current device ID to force position-based selection
@@ -845,4 +1169,480 @@ public class CameraXView implements LifecycleOwner {
845
1169
  previewView.setAlpha(opacity);
846
1170
  }
847
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
+ }
848
1648
  }