@capgo/camera-preview 7.4.0-beta.1 → 7.4.0-beta.11

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