@capgo/camera-preview 7.4.0-beta.1 → 7.4.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -31
- package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +5 -3
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +282 -45
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +902 -102
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +82 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +19 -5
- package/dist/docs.json +235 -6
- package/dist/esm/definitions.d.ts +119 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +47 -3
- package/dist/esm/web.js +262 -78
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +258 -78
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +258 -78
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +245 -28
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +657 -90
- package/package.json +1 -1
|
@@ -4,8 +4,8 @@ import android.content.Context;
|
|
|
4
4
|
import android.hardware.camera2.CameraAccessException;
|
|
5
5
|
import android.hardware.camera2.CameraManager;
|
|
6
6
|
import android.os.Build;
|
|
7
|
-
import android.os.HandlerThread;
|
|
8
7
|
import android.util.Base64;
|
|
8
|
+
import android.util.DisplayMetrics;
|
|
9
9
|
import android.util.Log;
|
|
10
10
|
import android.util.Size;
|
|
11
11
|
import android.view.ViewGroup;
|
|
@@ -19,6 +19,8 @@ import androidx.camera.core.ImageCapture;
|
|
|
19
19
|
import androidx.camera.core.ImageCaptureException;
|
|
20
20
|
import androidx.camera.core.ImageProxy;
|
|
21
21
|
import androidx.camera.core.Preview;
|
|
22
|
+
import androidx.camera.core.AspectRatio;
|
|
23
|
+
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
|
|
22
24
|
import androidx.camera.core.resolutionselector.ResolutionSelector;
|
|
23
25
|
import androidx.camera.core.resolutionselector.ResolutionStrategy;
|
|
24
26
|
import androidx.camera.lifecycle.ProcessCameraProvider;
|
|
@@ -28,37 +30,56 @@ import androidx.lifecycle.Lifecycle;
|
|
|
28
30
|
import androidx.lifecycle.LifecycleOwner;
|
|
29
31
|
import androidx.lifecycle.LifecycleRegistry;
|
|
30
32
|
import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
|
|
33
|
+
import android.widget.FrameLayout;
|
|
31
34
|
import com.ahm.capacitor.camera.preview.model.LensInfo;
|
|
32
35
|
import com.ahm.capacitor.camera.preview.model.ZoomFactors;
|
|
33
36
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
34
37
|
import java.nio.ByteBuffer;
|
|
35
|
-
import java.nio.file.Files;
|
|
36
38
|
import java.util.Arrays;
|
|
37
39
|
import java.util.Collections;
|
|
38
40
|
import java.util.List;
|
|
39
41
|
import java.util.ArrayList;
|
|
40
42
|
import java.util.Objects;
|
|
41
|
-
import java.util.concurrent.ExecutionException;
|
|
42
43
|
import java.util.concurrent.Executor;
|
|
43
44
|
import java.util.concurrent.ExecutorService;
|
|
44
45
|
import java.util.concurrent.Executors;
|
|
45
46
|
import androidx.camera.camera2.interop.Camera2CameraInfo;
|
|
46
47
|
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
|
|
47
48
|
import android.hardware.camera2.CameraCharacteristics;
|
|
48
|
-
import androidx.camera.extensions.ExtensionMode;
|
|
49
49
|
import java.util.Set;
|
|
50
50
|
import androidx.camera.core.ZoomState;
|
|
51
51
|
import androidx.camera.core.ResolutionInfo;
|
|
52
|
+
import android.content.Intent;
|
|
53
|
+
import android.net.Uri;
|
|
54
|
+
import android.os.Environment;
|
|
55
|
+
import java.io.File;
|
|
56
|
+
import java.io.FileOutputStream;
|
|
57
|
+
import java.io.IOException;
|
|
58
|
+
import java.text.SimpleDateFormat;
|
|
59
|
+
import java.util.Locale;
|
|
60
|
+
import androidx.exifinterface.media.ExifInterface;
|
|
61
|
+
import org.json.JSONObject;
|
|
62
|
+
import java.nio.file.Files;
|
|
63
|
+
import android.graphics.BitmapFactory;
|
|
64
|
+
import android.graphics.Bitmap;
|
|
65
|
+
import android.graphics.Matrix;
|
|
66
|
+
import java.io.ByteArrayOutputStream;
|
|
67
|
+
import android.location.Location;
|
|
68
|
+
import android.widget.FrameLayout;
|
|
69
|
+
import androidx.lifecycle.LifecycleObserver;
|
|
70
|
+
import androidx.lifecycle.OnLifecycleEvent;
|
|
71
|
+
import android.util.Rational;
|
|
72
|
+
import android.view.ViewGroup;
|
|
52
73
|
|
|
53
|
-
public class CameraXView implements LifecycleOwner {
|
|
74
|
+
public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
54
75
|
private static final String TAG = "CameraPreview CameraXView";
|
|
55
76
|
|
|
56
77
|
public interface CameraXViewListener {
|
|
57
|
-
void onPictureTaken(String
|
|
78
|
+
void onPictureTaken(String base64, JSONObject exif);
|
|
58
79
|
void onPictureTakenError(String message);
|
|
59
80
|
void onSampleTaken(String result);
|
|
60
81
|
void onSampleTakenError(String message);
|
|
61
|
-
void onCameraStarted();
|
|
82
|
+
void onCameraStarted(int width, int height, int x, int y);
|
|
62
83
|
void onCameraStartError(String message);
|
|
63
84
|
}
|
|
64
85
|
|
|
@@ -67,6 +88,8 @@ public class CameraXView implements LifecycleOwner {
|
|
|
67
88
|
private ImageCapture imageCapture;
|
|
68
89
|
private ImageCapture sampleImageCapture;
|
|
69
90
|
private PreviewView previewView;
|
|
91
|
+
private GridOverlayView gridOverlayView;
|
|
92
|
+
private FrameLayout previewContainer;
|
|
70
93
|
private CameraSelector currentCameraSelector;
|
|
71
94
|
private String currentDeviceId;
|
|
72
95
|
private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
|
|
@@ -102,6 +125,26 @@ public class CameraXView implements LifecycleOwner {
|
|
|
102
125
|
return isRunning;
|
|
103
126
|
}
|
|
104
127
|
|
|
128
|
+
private void saveImageToGallery(byte[] data) {
|
|
129
|
+
try {
|
|
130
|
+
File photo = new File(
|
|
131
|
+
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
|
132
|
+
"IMG_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new java.util.Date()) + ".jpg"
|
|
133
|
+
);
|
|
134
|
+
FileOutputStream fos = new FileOutputStream(photo);
|
|
135
|
+
fos.write(data);
|
|
136
|
+
fos.close();
|
|
137
|
+
|
|
138
|
+
// Notify the gallery of the new image
|
|
139
|
+
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
|
140
|
+
Uri contentUri = Uri.fromFile(photo);
|
|
141
|
+
mediaScanIntent.setData(contentUri);
|
|
142
|
+
context.sendBroadcast(mediaScanIntent);
|
|
143
|
+
} catch (IOException e) {
|
|
144
|
+
Log.e(TAG, "Error saving image to gallery", e);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
105
148
|
public void startSession(CameraSessionConfiguration config) {
|
|
106
149
|
this.sessionConfig = config;
|
|
107
150
|
cameraExecutor = Executors.newSingleThreadExecutor();
|
|
@@ -147,35 +190,140 @@ public class CameraXView implements LifecycleOwner {
|
|
|
147
190
|
if (sessionConfig.isToBack()) {
|
|
148
191
|
webView.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
|
149
192
|
}
|
|
193
|
+
|
|
194
|
+
// Create a container to hold both the preview and grid overlay
|
|
195
|
+
previewContainer = new FrameLayout(context);
|
|
196
|
+
|
|
197
|
+
// Create and setup the preview view
|
|
150
198
|
previewView = new PreviewView(context);
|
|
151
|
-
previewView.setScaleType(PreviewView.ScaleType.
|
|
199
|
+
previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
|
|
200
|
+
previewContainer.addView(previewView, new FrameLayout.LayoutParams(
|
|
201
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
202
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
203
|
+
));
|
|
204
|
+
|
|
205
|
+
// Create and setup the grid overlay
|
|
206
|
+
gridOverlayView = new GridOverlayView(context);
|
|
207
|
+
previewContainer.addView(gridOverlayView, new FrameLayout.LayoutParams(
|
|
208
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
209
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
210
|
+
));
|
|
211
|
+
// Set grid mode after adding to container to ensure proper layout
|
|
212
|
+
gridOverlayView.post(() -> {
|
|
213
|
+
String currentGridMode = sessionConfig.getGridMode();
|
|
214
|
+
Log.d(TAG, "setupPreviewView: Setting grid mode to: " + currentGridMode);
|
|
215
|
+
gridOverlayView.setGridMode(currentGridMode);
|
|
216
|
+
});
|
|
217
|
+
|
|
152
218
|
ViewGroup parent = (ViewGroup) webView.getParent();
|
|
153
219
|
if (parent != null) {
|
|
154
|
-
|
|
220
|
+
FrameLayout.LayoutParams layoutParams = calculatePreviewLayoutParams();
|
|
221
|
+
parent.addView(previewContainer, layoutParams);
|
|
155
222
|
if(sessionConfig.isToBack()) webView.bringToFront();
|
|
156
223
|
}
|
|
157
224
|
}
|
|
158
225
|
|
|
226
|
+
private FrameLayout.LayoutParams calculatePreviewLayoutParams() {
|
|
227
|
+
// sessionConfig already contains pixel-converted coordinates with webview offsets applied
|
|
228
|
+
int x = sessionConfig.getX();
|
|
229
|
+
int y = sessionConfig.getY();
|
|
230
|
+
int width = sessionConfig.getWidth();
|
|
231
|
+
int height = sessionConfig.getHeight();
|
|
232
|
+
String aspectRatio = sessionConfig.getAspectRatio();
|
|
233
|
+
|
|
234
|
+
Log.d(TAG, "calculatePreviewLayoutParams: Using sessionConfig values - x:" + x + " y:" + y + " width:" + width + " height:" + height + " aspectRatio:" + aspectRatio);
|
|
235
|
+
|
|
236
|
+
// Apply aspect ratio if specified and no explicit size was given
|
|
237
|
+
if (aspectRatio != null && !aspectRatio.isEmpty()) {
|
|
238
|
+
String[] ratios = aspectRatio.split(":");
|
|
239
|
+
if (ratios.length == 2) {
|
|
240
|
+
try {
|
|
241
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
242
|
+
float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
|
|
243
|
+
|
|
244
|
+
// Calculate optimal size while maintaining aspect ratio
|
|
245
|
+
int optimalWidth = width;
|
|
246
|
+
int optimalHeight = (int) (width / ratio);
|
|
247
|
+
|
|
248
|
+
if (optimalHeight > height) {
|
|
249
|
+
// Height constraint is tighter, fit by height
|
|
250
|
+
optimalHeight = height;
|
|
251
|
+
optimalWidth = (int) (height * ratio);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
width = optimalWidth;
|
|
255
|
+
height = optimalHeight;
|
|
256
|
+
Log.d(TAG, "calculatePreviewLayoutParams: Applied aspect ratio " + aspectRatio + " - new size: " + width + "x" + height);
|
|
257
|
+
} catch (NumberFormatException e) {
|
|
258
|
+
Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
|
|
264
|
+
|
|
265
|
+
// Only add insets for positioning coordinates, not for full-screen sizes
|
|
266
|
+
int webViewTopInset = getWebViewTopInset();
|
|
267
|
+
int webViewLeftInset = getWebViewLeftInset();
|
|
268
|
+
|
|
269
|
+
// Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
|
|
270
|
+
if (x == 0 && y == 0) {
|
|
271
|
+
layoutParams.leftMargin = x;
|
|
272
|
+
layoutParams.topMargin = y;
|
|
273
|
+
Log.d(TAG, "calculatePreviewLayoutParams: Full-screen mode - keeping position (0,0) without insets");
|
|
274
|
+
} else {
|
|
275
|
+
layoutParams.leftMargin = x + webViewLeftInset;
|
|
276
|
+
layoutParams.topMargin = y + webViewTopInset;
|
|
277
|
+
Log.d(TAG, "calculatePreviewLayoutParams: Positioned mode - applying insets");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
Log.d(TAG, "calculatePreviewLayoutParams: Applied insets - x:" + x + "+" + webViewLeftInset + "=" + layoutParams.leftMargin +
|
|
281
|
+
", y:" + y + "+" + webViewTopInset + "=" + layoutParams.topMargin);
|
|
282
|
+
|
|
283
|
+
Log.d(TAG, "calculatePreviewLayoutParams: Final layout - x:" + x + " y:" + y + " width:" + width + " height:" + height);
|
|
284
|
+
return layoutParams;
|
|
285
|
+
}
|
|
286
|
+
|
|
159
287
|
private void removePreviewView() {
|
|
160
|
-
if (
|
|
161
|
-
ViewGroup parent = (ViewGroup)
|
|
288
|
+
if (previewContainer != null) {
|
|
289
|
+
ViewGroup parent = (ViewGroup) previewContainer.getParent();
|
|
162
290
|
if (parent != null) {
|
|
163
|
-
parent.removeView(
|
|
291
|
+
parent.removeView(previewContainer);
|
|
164
292
|
}
|
|
293
|
+
previewContainer = null;
|
|
294
|
+
}
|
|
295
|
+
if (previewView != null) {
|
|
165
296
|
previewView = null;
|
|
166
297
|
}
|
|
298
|
+
if (gridOverlayView != null) {
|
|
299
|
+
gridOverlayView = null;
|
|
300
|
+
}
|
|
167
301
|
webView.setBackgroundColor(android.graphics.Color.WHITE);
|
|
168
302
|
}
|
|
169
303
|
|
|
304
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
170
305
|
private void bindCameraUseCases() {
|
|
171
306
|
if (cameraProvider == null) return;
|
|
172
307
|
mainExecutor.execute(() -> {
|
|
173
308
|
try {
|
|
174
309
|
Log.d(TAG, "Building camera selector with deviceId: " + sessionConfig.getDeviceId() + " and position: " + sessionConfig.getPosition());
|
|
175
310
|
currentCameraSelector = buildCameraSelector();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.
|
|
311
|
+
|
|
312
|
+
ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder()
|
|
313
|
+
.setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY);
|
|
314
|
+
|
|
315
|
+
if (sessionConfig.getAspectRatio() != null) {
|
|
316
|
+
int aspectRatio;
|
|
317
|
+
if ("16:9".equals(sessionConfig.getAspectRatio())) {
|
|
318
|
+
aspectRatio = AspectRatio.RATIO_16_9;
|
|
319
|
+
} else { // "4:3"
|
|
320
|
+
aspectRatio = AspectRatio.RATIO_4_3;
|
|
321
|
+
}
|
|
322
|
+
resolutionSelectorBuilder.setAspectRatioStrategy(new AspectRatioStrategy(aspectRatio, AspectRatioStrategy.FALLBACK_RULE_AUTO));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
ResolutionSelector resolutionSelector = resolutionSelectorBuilder.build();
|
|
326
|
+
|
|
179
327
|
Preview preview = new Preview.Builder().setResolutionSelector(resolutionSelector).build();
|
|
180
328
|
imageCapture = new ImageCapture.Builder().setResolutionSelector(resolutionSelector).setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).setFlashMode(currentFlashMode).build();
|
|
181
329
|
sampleImageCapture = imageCapture;
|
|
@@ -188,7 +336,6 @@ public class CameraXView implements LifecycleOwner {
|
|
|
188
336
|
Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
|
|
189
337
|
CameraInfo cameraInfo = camera.getCameraInfo();
|
|
190
338
|
Log.d(TAG, "Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId());
|
|
191
|
-
Log.d(TAG, "Implementation Type: " + cameraInfo.getImplementationType());
|
|
192
339
|
|
|
193
340
|
// Log zoom state
|
|
194
341
|
ZoomState zoomState = cameraInfo.getZoomState().getValue();
|
|
@@ -227,7 +374,17 @@ public class CameraXView implements LifecycleOwner {
|
|
|
227
374
|
|
|
228
375
|
isRunning = true;
|
|
229
376
|
Log.d(TAG, "bindCameraUseCases: Camera bound successfully");
|
|
230
|
-
if (listener != null)
|
|
377
|
+
if (listener != null) {
|
|
378
|
+
// Post the callback to ensure layout is complete
|
|
379
|
+
previewContainer.post(() -> {
|
|
380
|
+
// Return actual preview container dimensions instead of requested dimensions
|
|
381
|
+
int actualWidth = previewContainer != null ? previewContainer.getWidth() : sessionConfig.getWidth();
|
|
382
|
+
int actualHeight = previewContainer != null ? previewContainer.getHeight() : sessionConfig.getHeight();
|
|
383
|
+
int actualX = previewContainer != null ? previewContainer.getLeft() : sessionConfig.getX();
|
|
384
|
+
int actualY = previewContainer != null ? previewContainer.getTop() : sessionConfig.getY();
|
|
385
|
+
listener.onCameraStarted(actualWidth, actualHeight, actualX, actualY);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
231
388
|
} catch (Exception e) {
|
|
232
389
|
if (listener != null) listener.onCameraStartError("Error binding camera: " + e.getMessage());
|
|
233
390
|
}
|
|
@@ -256,22 +413,6 @@ public class CameraXView implements LifecycleOwner {
|
|
|
256
413
|
return builder.build();
|
|
257
414
|
}
|
|
258
415
|
|
|
259
|
-
private static boolean isIsLogical(CameraManager cameraManager, String cameraId) throws CameraAccessException {
|
|
260
|
-
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
|
|
261
|
-
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
|
|
262
|
-
|
|
263
|
-
boolean isLogical = false;
|
|
264
|
-
if (capabilities != null) {
|
|
265
|
-
for (int capability : capabilities) {
|
|
266
|
-
if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) {
|
|
267
|
-
isLogical = true;
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return isLogical;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
416
|
private static String getCameraId(androidx.camera.core.CameraInfo cameraInfo) {
|
|
276
417
|
try {
|
|
277
418
|
// Generate a stable ID based on camera characteristics
|
|
@@ -303,7 +444,7 @@ public class CameraXView implements LifecycleOwner {
|
|
|
303
444
|
}
|
|
304
445
|
}
|
|
305
446
|
|
|
306
|
-
public void capturePhoto(int quality) {
|
|
447
|
+
public void capturePhoto(int quality, final boolean saveToGallery, Integer width, Integer height, Location location) {
|
|
307
448
|
Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality);
|
|
308
449
|
|
|
309
450
|
if (imageCapture == null) {
|
|
@@ -313,9 +454,8 @@ public class CameraXView implements LifecycleOwner {
|
|
|
313
454
|
return;
|
|
314
455
|
}
|
|
315
456
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
).build();
|
|
457
|
+
File tempFile = new File(context.getCacheDir(), "temp_image.jpg");
|
|
458
|
+
ImageCapture.OutputFileOptions outputFileOptions = new ImageCapture.OutputFileOptions.Builder(tempFile).build();
|
|
319
459
|
|
|
320
460
|
imageCapture.takePicture(
|
|
321
461
|
outputFileOptions,
|
|
@@ -331,31 +471,52 @@ public class CameraXView implements LifecycleOwner {
|
|
|
331
471
|
|
|
332
472
|
@Override
|
|
333
473
|
public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
|
|
334
|
-
// Convert to base64
|
|
335
474
|
try {
|
|
336
|
-
|
|
337
|
-
byte[] bytes;
|
|
475
|
+
// Read file using FileInputStream for compatibility
|
|
476
|
+
byte[] bytes = new byte[(int) tempFile.length()];
|
|
477
|
+
java.io.FileInputStream fis = new java.io.FileInputStream(tempFile);
|
|
478
|
+
fis.read(bytes);
|
|
479
|
+
fis.close();
|
|
480
|
+
|
|
481
|
+
ExifInterface exifInterface = new ExifInterface(tempFile.getAbsolutePath());
|
|
482
|
+
|
|
483
|
+
if (location != null) {
|
|
484
|
+
exifInterface.setGpsInfo(location);
|
|
485
|
+
}
|
|
338
486
|
|
|
339
|
-
|
|
340
|
-
|
|
487
|
+
JSONObject exifData = getExifData(exifInterface);
|
|
488
|
+
|
|
489
|
+
if (width != null && height != null) {
|
|
490
|
+
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
|
491
|
+
Bitmap resizedBitmap = resizeBitmap(bitmap, width, height);
|
|
492
|
+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
493
|
+
resizedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
|
|
494
|
+
bytes = stream.toByteArray();
|
|
495
|
+
|
|
496
|
+
// Write EXIF data back to resized image
|
|
497
|
+
bytes = writeExifToImageBytes(bytes, exifInterface);
|
|
341
498
|
} else {
|
|
342
|
-
//
|
|
343
|
-
|
|
499
|
+
// For non-resized images, ensure EXIF is saved
|
|
500
|
+
exifInterface.saveAttributes();
|
|
344
501
|
bytes = new byte[(int) tempFile.length()];
|
|
345
|
-
|
|
346
|
-
|
|
502
|
+
java.io.FileInputStream fis2 = new java.io.FileInputStream(tempFile);
|
|
503
|
+
fis2.read(bytes);
|
|
504
|
+
fis2.close();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (saveToGallery) {
|
|
508
|
+
saveImageToGallery(bytes);
|
|
347
509
|
}
|
|
348
510
|
|
|
349
511
|
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
350
512
|
|
|
351
|
-
// Clean up temp file
|
|
352
513
|
tempFile.delete();
|
|
353
514
|
|
|
354
515
|
if (listener != null) {
|
|
355
|
-
listener.onPictureTaken(base64);
|
|
516
|
+
listener.onPictureTaken(base64, exifData);
|
|
356
517
|
}
|
|
357
518
|
} catch (Exception e) {
|
|
358
|
-
Log.e(TAG, "capturePhoto: Error
|
|
519
|
+
Log.e(TAG, "capturePhoto: Error processing image", e);
|
|
359
520
|
if (listener != null) {
|
|
360
521
|
listener.onPictureTakenError("Error processing image: " + e.getMessage());
|
|
361
522
|
}
|
|
@@ -365,6 +526,210 @@ public class CameraXView implements LifecycleOwner {
|
|
|
365
526
|
);
|
|
366
527
|
}
|
|
367
528
|
|
|
529
|
+
private Bitmap resizeBitmap(Bitmap bitmap, int width, int height) {
|
|
530
|
+
return Bitmap.createScaledBitmap(bitmap, width, height, true);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private JSONObject getExifData(ExifInterface exifInterface) {
|
|
534
|
+
JSONObject exifData = new JSONObject();
|
|
535
|
+
try {
|
|
536
|
+
// Add all available exif tags to a JSON object
|
|
537
|
+
for (String[] tag : EXIF_TAGS) {
|
|
538
|
+
String value = exifInterface.getAttribute(tag[0]);
|
|
539
|
+
if (value != null) {
|
|
540
|
+
exifData.put(tag[1], value);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch (Exception e) {
|
|
544
|
+
Log.e(TAG, "getExifData: Error reading exif data", e);
|
|
545
|
+
}
|
|
546
|
+
return exifData;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private static final String[][] EXIF_TAGS = new String[][]{
|
|
550
|
+
{ExifInterface.TAG_APERTURE_VALUE, "ApertureValue"},
|
|
551
|
+
{ExifInterface.TAG_ARTIST, "Artist"},
|
|
552
|
+
{ExifInterface.TAG_BITS_PER_SAMPLE, "BitsPerSample"},
|
|
553
|
+
{ExifInterface.TAG_BRIGHTNESS_VALUE, "BrightnessValue"},
|
|
554
|
+
{ExifInterface.TAG_CFA_PATTERN, "CFAPattern"},
|
|
555
|
+
{ExifInterface.TAG_COLOR_SPACE, "ColorSpace"},
|
|
556
|
+
{ExifInterface.TAG_COMPONENTS_CONFIGURATION, "ComponentsConfiguration"},
|
|
557
|
+
{ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, "CompressedBitsPerPixel"},
|
|
558
|
+
{ExifInterface.TAG_COMPRESSION, "Compression"},
|
|
559
|
+
{ExifInterface.TAG_CONTRAST, "Contrast"},
|
|
560
|
+
{ExifInterface.TAG_COPYRIGHT, "Copyright"},
|
|
561
|
+
{ExifInterface.TAG_CUSTOM_RENDERED, "CustomRendered"},
|
|
562
|
+
{ExifInterface.TAG_DATETIME, "DateTime"},
|
|
563
|
+
{ExifInterface.TAG_DATETIME_DIGITIZED, "DateTimeDigitized"},
|
|
564
|
+
{ExifInterface.TAG_DATETIME_ORIGINAL, "DateTimeOriginal"},
|
|
565
|
+
{ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, "DeviceSettingDescription"},
|
|
566
|
+
{ExifInterface.TAG_DIGITAL_ZOOM_RATIO, "DigitalZoomRatio"},
|
|
567
|
+
{ExifInterface.TAG_DNG_VERSION, "DNGVersion"},
|
|
568
|
+
{ExifInterface.TAG_EXIF_VERSION, "ExifVersion"},
|
|
569
|
+
{ExifInterface.TAG_EXPOSURE_BIAS_VALUE, "ExposureBiasValue"},
|
|
570
|
+
{ExifInterface.TAG_EXPOSURE_INDEX, "ExposureIndex"},
|
|
571
|
+
{ExifInterface.TAG_EXPOSURE_MODE, "ExposureMode"},
|
|
572
|
+
{ExifInterface.TAG_EXPOSURE_PROGRAM, "ExposureProgram"},
|
|
573
|
+
{ExifInterface.TAG_EXPOSURE_TIME, "ExposureTime"},
|
|
574
|
+
{ExifInterface.TAG_FILE_SOURCE, "FileSource"},
|
|
575
|
+
{ExifInterface.TAG_FLASH, "Flash"},
|
|
576
|
+
{ExifInterface.TAG_FLASHPIX_VERSION, "FlashpixVersion"},
|
|
577
|
+
{ExifInterface.TAG_FLASH_ENERGY, "FlashEnergy"},
|
|
578
|
+
{ExifInterface.TAG_FOCAL_LENGTH, "FocalLength"},
|
|
579
|
+
{ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, "FocalLengthIn35mmFilm"},
|
|
580
|
+
{ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, "FocalPlaneResolutionUnit"},
|
|
581
|
+
{ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, "FocalPlaneXResolution"},
|
|
582
|
+
{ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, "FocalPlaneYResolution"},
|
|
583
|
+
{ExifInterface.TAG_F_NUMBER, "FNumber"},
|
|
584
|
+
{ExifInterface.TAG_GAIN_CONTROL, "GainControl"},
|
|
585
|
+
{ExifInterface.TAG_GPS_ALTITUDE, "GPSAltitude"},
|
|
586
|
+
{ExifInterface.TAG_GPS_ALTITUDE_REF, "GPSAltitudeRef"},
|
|
587
|
+
{ExifInterface.TAG_GPS_AREA_INFORMATION, "GPSAreaInformation"},
|
|
588
|
+
{ExifInterface.TAG_GPS_DATESTAMP, "GPSDateStamp"},
|
|
589
|
+
{ExifInterface.TAG_GPS_DEST_BEARING, "GPSDestBearing"},
|
|
590
|
+
{ExifInterface.TAG_GPS_DEST_BEARING_REF, "GPSDestBearingRef"},
|
|
591
|
+
{ExifInterface.TAG_GPS_DEST_DISTANCE, "GPSDestDistance"},
|
|
592
|
+
{ExifInterface.TAG_GPS_DEST_DISTANCE_REF, "GPSDestDistanceRef"},
|
|
593
|
+
{ExifInterface.TAG_GPS_DEST_LATITUDE, "GPSDestLatitude"},
|
|
594
|
+
{ExifInterface.TAG_GPS_DEST_LATITUDE_REF, "GPSDestLatitudeRef"},
|
|
595
|
+
{ExifInterface.TAG_GPS_DEST_LONGITUDE, "GPSDestLongitude"},
|
|
596
|
+
{ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, "GPSDestLongitudeRef"},
|
|
597
|
+
{ExifInterface.TAG_GPS_DIFFERENTIAL, "GPSDifferential"},
|
|
598
|
+
{ExifInterface.TAG_GPS_DOP, "GPSDOP"},
|
|
599
|
+
{ExifInterface.TAG_GPS_IMG_DIRECTION, "GPSImgDirection"},
|
|
600
|
+
{ExifInterface.TAG_GPS_IMG_DIRECTION_REF, "GPSImgDirectionRef"},
|
|
601
|
+
{ExifInterface.TAG_GPS_LATITUDE, "GPSLatitude"},
|
|
602
|
+
{ExifInterface.TAG_GPS_LATITUDE_REF, "GPSLatitudeRef"},
|
|
603
|
+
{ExifInterface.TAG_GPS_LONGITUDE, "GPSLongitude"},
|
|
604
|
+
{ExifInterface.TAG_GPS_LONGITUDE_REF, "GPSLongitudeRef"},
|
|
605
|
+
{ExifInterface.TAG_GPS_MAP_DATUM, "GPSMapDatum"},
|
|
606
|
+
{ExifInterface.TAG_GPS_MEASURE_MODE, "GPSMeasureMode"},
|
|
607
|
+
{ExifInterface.TAG_GPS_PROCESSING_METHOD, "GPSProcessingMethod"},
|
|
608
|
+
{ExifInterface.TAG_GPS_SATELLITES, "GPSSatellites"},
|
|
609
|
+
{ExifInterface.TAG_GPS_SPEED, "GPSSpeed"},
|
|
610
|
+
{ExifInterface.TAG_GPS_SPEED_REF, "GPSSpeedRef"},
|
|
611
|
+
{ExifInterface.TAG_GPS_STATUS, "GPSStatus"},
|
|
612
|
+
{ExifInterface.TAG_GPS_TIMESTAMP, "GPSTimeStamp"},
|
|
613
|
+
{ExifInterface.TAG_GPS_TRACK, "GPSTrack"},
|
|
614
|
+
{ExifInterface.TAG_GPS_TRACK_REF, "GPSTrackRef"},
|
|
615
|
+
{ExifInterface.TAG_GPS_VERSION_ID, "GPSVersionID"},
|
|
616
|
+
{ExifInterface.TAG_IMAGE_DESCRIPTION, "ImageDescription"},
|
|
617
|
+
{ExifInterface.TAG_IMAGE_LENGTH, "ImageLength"},
|
|
618
|
+
{ExifInterface.TAG_IMAGE_UNIQUE_ID, "ImageUniqueID"},
|
|
619
|
+
{ExifInterface.TAG_IMAGE_WIDTH, "ImageWidth"},
|
|
620
|
+
{ExifInterface.TAG_INTEROPERABILITY_INDEX, "InteroperabilityIndex"},
|
|
621
|
+
{ExifInterface.TAG_ISO_SPEED, "ISOSpeed"},
|
|
622
|
+
{ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, "ISOSpeedLatitudeyyy"},
|
|
623
|
+
{ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, "ISOSpeedLatitudezzz"},
|
|
624
|
+
{ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, "JPEGInterchangeFormat"},
|
|
625
|
+
{ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, "JPEGInterchangeFormatLength"},
|
|
626
|
+
{ExifInterface.TAG_LIGHT_SOURCE, "LightSource"},
|
|
627
|
+
{ExifInterface.TAG_MAKE, "Make"},
|
|
628
|
+
{ExifInterface.TAG_MAKER_NOTE, "MakerNote"},
|
|
629
|
+
{ExifInterface.TAG_MAX_APERTURE_VALUE, "MaxApertureValue"},
|
|
630
|
+
{ExifInterface.TAG_METERING_MODE, "MeteringMode"},
|
|
631
|
+
{ExifInterface.TAG_MODEL, "Model"},
|
|
632
|
+
{ExifInterface.TAG_NEW_SUBFILE_TYPE, "NewSubfileType"},
|
|
633
|
+
{ExifInterface.TAG_OECF, "OECF"},
|
|
634
|
+
{ExifInterface.TAG_OFFSET_TIME, "OffsetTime"},
|
|
635
|
+
{ExifInterface.TAG_OFFSET_TIME_DIGITIZED, "OffsetTimeDigitized"},
|
|
636
|
+
{ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "OffsetTimeOriginal"},
|
|
637
|
+
{ExifInterface.TAG_ORF_ASPECT_FRAME, "ORFAspectFrame"},
|
|
638
|
+
{ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH, "ORFPreviewImageLength"},
|
|
639
|
+
{ExifInterface.TAG_ORF_PREVIEW_IMAGE_START, "ORFPreviewImageStart"},
|
|
640
|
+
{ExifInterface.TAG_ORF_THUMBNAIL_IMAGE, "ORFThumbnailImage"},
|
|
641
|
+
{ExifInterface.TAG_ORIENTATION, "Orientation"},
|
|
642
|
+
{ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation"},
|
|
643
|
+
{ExifInterface.TAG_PIXEL_X_DIMENSION, "PixelXDimension"},
|
|
644
|
+
{ExifInterface.TAG_PIXEL_Y_DIMENSION, "PixelYDimension"},
|
|
645
|
+
{ExifInterface.TAG_PLANAR_CONFIGURATION, "PlanarConfiguration"},
|
|
646
|
+
{ExifInterface.TAG_PRIMARY_CHROMATICITIES, "PrimaryChromaticities"},
|
|
647
|
+
{ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, "RecommendedExposureIndex"},
|
|
648
|
+
{ExifInterface.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite"},
|
|
649
|
+
{ExifInterface.TAG_RELATED_SOUND_FILE, "RelatedSoundFile"},
|
|
650
|
+
{ExifInterface.TAG_RESOLUTION_UNIT, "ResolutionUnit"},
|
|
651
|
+
{ExifInterface.TAG_ROWS_PER_STRIP, "RowsPerStrip"},
|
|
652
|
+
{ExifInterface.TAG_RW2_ISO, "RW2ISO"},
|
|
653
|
+
{ExifInterface.TAG_RW2_JPG_FROM_RAW, "RW2JpgFromRaw"},
|
|
654
|
+
{ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER, "RW2SensorBottomBorder"},
|
|
655
|
+
{ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER, "RW2SensorLeftBorder"},
|
|
656
|
+
{ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, "RW2SensorRightBorder"},
|
|
657
|
+
{ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, "RW2SensorTopBorder"},
|
|
658
|
+
{ExifInterface.TAG_SAMPLES_PER_PIXEL, "SamplesPerPixel"},
|
|
659
|
+
{ExifInterface.TAG_SATURATION, "Saturation"},
|
|
660
|
+
{ExifInterface.TAG_SCENE_CAPTURE_TYPE, "SceneCaptureType"},
|
|
661
|
+
{ExifInterface.TAG_SCENE_TYPE, "SceneType"},
|
|
662
|
+
{ExifInterface.TAG_SENSING_METHOD, "SensingMethod"},
|
|
663
|
+
{ExifInterface.TAG_SENSITIVITY_TYPE, "SensitivityType"},
|
|
664
|
+
{ExifInterface.TAG_SHARPNESS, "Sharpness"},
|
|
665
|
+
{ExifInterface.TAG_SHUTTER_SPEED_VALUE, "ShutterSpeedValue"},
|
|
666
|
+
{ExifInterface.TAG_SOFTWARE, "Software"},
|
|
667
|
+
{ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, "SpatialFrequencyResponse"},
|
|
668
|
+
{ExifInterface.TAG_SPECTRAL_SENSITIVITY, "SpectralSensitivity"},
|
|
669
|
+
{ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, "StandardOutputSensitivity"},
|
|
670
|
+
{ExifInterface.TAG_STRIP_BYTE_COUNTS, "StripByteCounts"},
|
|
671
|
+
{ExifInterface.TAG_STRIP_OFFSETS, "StripOffsets"},
|
|
672
|
+
{ExifInterface.TAG_SUBFILE_TYPE, "SubfileType"},
|
|
673
|
+
{ExifInterface.TAG_SUBJECT_AREA, "SubjectArea"},
|
|
674
|
+
{ExifInterface.TAG_SUBJECT_DISTANCE, "SubjectDistance"},
|
|
675
|
+
{ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, "SubjectDistanceRange"},
|
|
676
|
+
{ExifInterface.TAG_SUBJECT_LOCATION, "SubjectLocation"},
|
|
677
|
+
{ExifInterface.TAG_SUBSEC_TIME, "SubSecTime"},
|
|
678
|
+
{ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, "SubSecTimeDigitized"},
|
|
679
|
+
{ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, "SubSecTimeOriginal"},
|
|
680
|
+
{ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH, "ThumbnailImageLength"},
|
|
681
|
+
{ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH, "ThumbnailImageWidth"},
|
|
682
|
+
{ExifInterface.TAG_TRANSFER_FUNCTION, "TransferFunction"},
|
|
683
|
+
{ExifInterface.TAG_USER_COMMENT, "UserComment"},
|
|
684
|
+
{ExifInterface.TAG_WHITE_BALANCE, "WhiteBalance"},
|
|
685
|
+
{ExifInterface.TAG_WHITE_POINT, "WhitePoint"},
|
|
686
|
+
{ExifInterface.TAG_X_RESOLUTION, "XResolution"},
|
|
687
|
+
{ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, "YCbCrCoefficients"},
|
|
688
|
+
{ExifInterface.TAG_Y_CB_CR_POSITIONING, "YCbCrPositioning"},
|
|
689
|
+
{ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, "YCbCrSubSampling"},
|
|
690
|
+
{ExifInterface.TAG_Y_RESOLUTION, "YResolution"}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
private byte[] writeExifToImageBytes(byte[] imageBytes, ExifInterface sourceExif) {
|
|
694
|
+
try {
|
|
695
|
+
// Create a temporary file to write the image with EXIF
|
|
696
|
+
File tempExifFile = File.createTempFile("temp_exif", ".jpg", context.getCacheDir());
|
|
697
|
+
|
|
698
|
+
// Write the image bytes to temp file
|
|
699
|
+
java.io.FileOutputStream fos = new java.io.FileOutputStream(tempExifFile);
|
|
700
|
+
fos.write(imageBytes);
|
|
701
|
+
fos.close();
|
|
702
|
+
|
|
703
|
+
// Create new ExifInterface for the temp file and copy all EXIF data
|
|
704
|
+
ExifInterface newExif = new ExifInterface(tempExifFile.getAbsolutePath());
|
|
705
|
+
|
|
706
|
+
// Copy all EXIF attributes from source to new
|
|
707
|
+
for (String[] tag : EXIF_TAGS) {
|
|
708
|
+
String value = sourceExif.getAttribute(tag[0]);
|
|
709
|
+
if (value != null) {
|
|
710
|
+
newExif.setAttribute(tag[0], value);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Save the EXIF data
|
|
715
|
+
newExif.saveAttributes();
|
|
716
|
+
|
|
717
|
+
// Read the file back with EXIF embedded
|
|
718
|
+
byte[] result = new byte[(int) tempExifFile.length()];
|
|
719
|
+
java.io.FileInputStream fis = new java.io.FileInputStream(tempExifFile);
|
|
720
|
+
fis.read(result);
|
|
721
|
+
fis.close();
|
|
722
|
+
|
|
723
|
+
// Clean up temp file
|
|
724
|
+
tempExifFile.delete();
|
|
725
|
+
|
|
726
|
+
return result;
|
|
727
|
+
} catch (Exception e) {
|
|
728
|
+
Log.e(TAG, "writeExifToImageBytes: Error writing EXIF data", e);
|
|
729
|
+
return imageBytes; // Return original bytes if error
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
368
733
|
public void captureSample(int quality) {
|
|
369
734
|
Log.d(TAG, "captureSample: Starting sample capture with quality: " + quality);
|
|
370
735
|
|
|
@@ -430,7 +795,7 @@ public class CameraXView implements LifecycleOwner {
|
|
|
430
795
|
for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
|
|
431
796
|
String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
|
|
432
797
|
String position = isBackCamera(cameraInfo) ? "rear" : "front";
|
|
433
|
-
|
|
798
|
+
|
|
434
799
|
// Add logical camera
|
|
435
800
|
float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
|
|
436
801
|
float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
|
|
@@ -466,7 +831,7 @@ public class CameraXView implements LifecycleOwner {
|
|
|
466
831
|
if (focalLengths[0] < 3.0f) deviceType = "ultraWide";
|
|
467
832
|
else if (focalLengths[0] > 5.0f) deviceType = "telephoto";
|
|
468
833
|
}
|
|
469
|
-
|
|
834
|
+
|
|
470
835
|
float physicalMinZoom = 1.0f;
|
|
471
836
|
float physicalMaxZoom = 1.0f;
|
|
472
837
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
@@ -476,11 +841,11 @@ public class CameraXView implements LifecycleOwner {
|
|
|
476
841
|
physicalMaxZoom = zoomRange.getUpper();
|
|
477
842
|
}
|
|
478
843
|
}
|
|
479
|
-
|
|
844
|
+
|
|
480
845
|
String label = "Physical " + deviceType + " (" + position + ")";
|
|
481
846
|
List<LensInfo> physicalLenses = new ArrayList<>();
|
|
482
847
|
physicalLenses.add(new LensInfo(focalLengths != null ? focalLengths[0] : 4.25f, deviceType, 1.0f, physicalMaxZoom));
|
|
483
|
-
|
|
848
|
+
|
|
484
849
|
devices.add(new com.ahm.capacitor.camera.preview.model.CameraDevice(
|
|
485
850
|
physicalId, label, position, physicalLenses, physicalMinZoom, physicalMaxZoom, false
|
|
486
851
|
));
|
|
@@ -498,11 +863,10 @@ public class CameraXView implements LifecycleOwner {
|
|
|
498
863
|
}
|
|
499
864
|
}
|
|
500
865
|
|
|
501
|
-
public static ZoomFactors getZoomFactorsStatic(
|
|
866
|
+
public static ZoomFactors getZoomFactorsStatic() {
|
|
502
867
|
try {
|
|
503
868
|
// For static method, return default zoom factors
|
|
504
869
|
// We can try to detect if ultra-wide is available by checking device list
|
|
505
|
-
List<com.ahm.capacitor.camera.preview.model.CameraDevice> devices = getAvailableDevicesStatic(context);
|
|
506
870
|
|
|
507
871
|
float minZoom = 1.0f;
|
|
508
872
|
float maxZoom = 10.0f;
|
|
@@ -519,7 +883,7 @@ public class CameraXView implements LifecycleOwner {
|
|
|
519
883
|
|
|
520
884
|
public ZoomFactors getZoomFactors() {
|
|
521
885
|
if (camera == null) {
|
|
522
|
-
return getZoomFactorsStatic(
|
|
886
|
+
return getZoomFactorsStatic();
|
|
523
887
|
}
|
|
524
888
|
|
|
525
889
|
try {
|
|
@@ -546,8 +910,6 @@ public class CameraXView implements LifecycleOwner {
|
|
|
546
910
|
|
|
547
911
|
try {
|
|
548
912
|
float currentZoom = Objects.requireNonNull(camera.getCameraInfo().getZoomState().getValue()).getZoomRatio();
|
|
549
|
-
float minZoom = camera.getCameraInfo().getZoomState().getValue().getMinZoomRatio();
|
|
550
|
-
float maxZoom = camera.getCameraInfo().getZoomState().getValue().getMaxZoomRatio();
|
|
551
913
|
|
|
552
914
|
// Determine device type based on zoom capabilities
|
|
553
915
|
String deviceType = "wideAngle";
|
|
@@ -594,46 +956,6 @@ public class CameraXView implements LifecycleOwner {
|
|
|
594
956
|
}
|
|
595
957
|
}
|
|
596
958
|
|
|
597
|
-
private List<androidx.camera.core.CameraInfo> getAvailableCamerasForCurrentPosition() {
|
|
598
|
-
if (cameraProvider == null) {
|
|
599
|
-
Log.w(TAG, "getAvailableCamerasForCurrentPosition: cameraProvider is null");
|
|
600
|
-
return Collections.emptyList();
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
List<androidx.camera.core.CameraInfo> allCameras = cameraProvider.getAvailableCameraInfos();
|
|
604
|
-
List<androidx.camera.core.CameraInfo> sameFacingCameras = new ArrayList<>();
|
|
605
|
-
|
|
606
|
-
Log.d(TAG, "getAvailableCamerasForCurrentPosition: Total cameras available: " + allCameras.size());
|
|
607
|
-
|
|
608
|
-
// Determine current facing direction from the session config to avoid restricted API call
|
|
609
|
-
boolean isCurrentBack = "back".equals(sessionConfig.getPosition());
|
|
610
|
-
Log.d(TAG, "getAvailableCamerasForCurrentPosition: Looking for " + (isCurrentBack ? "back" : "front") + " cameras");
|
|
611
|
-
|
|
612
|
-
for (int i = 0; i < allCameras.size(); i++) {
|
|
613
|
-
androidx.camera.core.CameraInfo cameraInfo = allCameras.get(i);
|
|
614
|
-
boolean isCameraBack = isBackCamera(cameraInfo);
|
|
615
|
-
String cameraId = getCameraId(cameraInfo);
|
|
616
|
-
|
|
617
|
-
Log.d(TAG, "getAvailableCamerasForCurrentPosition: Camera " + i + " - ID: " + cameraId + ", isBack: " + isCameraBack);
|
|
618
|
-
|
|
619
|
-
try {
|
|
620
|
-
float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
|
|
621
|
-
float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
|
|
622
|
-
Log.d(TAG, "getAvailableCamerasForCurrentPosition: Camera " + i + " zoom range: " + minZoom + "-" + maxZoom);
|
|
623
|
-
} catch (Exception e) {
|
|
624
|
-
Log.w(TAG, "getAvailableCamerasForCurrentPosition: Cannot get zoom info for camera " + i + ": " + e.getMessage());
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
if (isCameraBack == isCurrentBack) {
|
|
628
|
-
sameFacingCameras.add(cameraInfo);
|
|
629
|
-
Log.d(TAG, "getAvailableCamerasForCurrentPosition: Added camera " + i + " (" + cameraId + ") to same-facing list");
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
Log.d(TAG, "getAvailableCamerasForCurrentPosition: Found " + sameFacingCameras.size() + " cameras for " + (isCurrentBack ? "back" : "front"));
|
|
634
|
-
return sameFacingCameras;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
959
|
public static List<Size> getSupportedPictureSizes(String facing) {
|
|
638
960
|
List<Size> sizes = new ArrayList<>();
|
|
639
961
|
try {
|
|
@@ -786,13 +1108,13 @@ public class CameraXView implements LifecycleOwner {
|
|
|
786
1108
|
Log.d(TAG, "switchToDevice: Found matching CameraInfo for deviceId: " + deviceId);
|
|
787
1109
|
final CameraInfo finalTarget = targetCameraInfo;
|
|
788
1110
|
|
|
789
|
-
|
|
1111
|
+
// This filter will receive a list of all cameras and must return the one we want.
|
|
1112
|
+
|
|
1113
|
+
currentCameraSelector = new CameraSelector.Builder()
|
|
790
1114
|
.addCameraFilter(cameras -> {
|
|
791
1115
|
// This filter will receive a list of all cameras and must return the one we want.
|
|
792
1116
|
return Collections.singletonList(finalTarget);
|
|
793
1117
|
}).build();
|
|
794
|
-
|
|
795
|
-
currentCameraSelector = newSelector;
|
|
796
1118
|
currentDeviceId = deviceId;
|
|
797
1119
|
bindCameraUseCases(); // Rebind with the new, highly specific selector
|
|
798
1120
|
} else {
|
|
@@ -827,7 +1149,9 @@ public class CameraXView implements LifecycleOwner {
|
|
|
827
1149
|
sessionConfig.isEnableZoom(), // enableZoom
|
|
828
1150
|
sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
|
|
829
1151
|
sessionConfig.isDisableAudio(), // disableAudio
|
|
830
|
-
sessionConfig.getZoomFactor() // zoomFactor
|
|
1152
|
+
sessionConfig.getZoomFactor(), // zoomFactor
|
|
1153
|
+
sessionConfig.getAspectRatio(), // aspectRatio
|
|
1154
|
+
sessionConfig.getGridMode() // gridMode
|
|
831
1155
|
);
|
|
832
1156
|
|
|
833
1157
|
// Clear current device ID to force position-based selection
|
|
@@ -845,4 +1169,480 @@ public class CameraXView implements LifecycleOwner {
|
|
|
845
1169
|
previewView.setAlpha(opacity);
|
|
846
1170
|
}
|
|
847
1171
|
}
|
|
1172
|
+
|
|
1173
|
+
private void updateLayoutParams() {
|
|
1174
|
+
if (sessionConfig == null) return;
|
|
1175
|
+
|
|
1176
|
+
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
|
|
1177
|
+
sessionConfig.getWidth(),
|
|
1178
|
+
sessionConfig.getHeight()
|
|
1179
|
+
);
|
|
1180
|
+
layoutParams.leftMargin = sessionConfig.getX();
|
|
1181
|
+
layoutParams.topMargin = sessionConfig.getY();
|
|
1182
|
+
|
|
1183
|
+
if (sessionConfig.getAspectRatio() != null) {
|
|
1184
|
+
String[] ratios = sessionConfig.getAspectRatio().split(":");
|
|
1185
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
1186
|
+
float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
|
|
1187
|
+
if (sessionConfig.getWidth() > 0) {
|
|
1188
|
+
layoutParams.height = (int) (sessionConfig.getWidth() / ratio);
|
|
1189
|
+
} else if (sessionConfig.getHeight() > 0) {
|
|
1190
|
+
layoutParams.width = (int) (sessionConfig.getHeight() * ratio);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
previewView.setLayoutParams(layoutParams);
|
|
1195
|
+
|
|
1196
|
+
if (listener != null) {
|
|
1197
|
+
listener.onCameraStarted(
|
|
1198
|
+
sessionConfig.getWidth(),
|
|
1199
|
+
sessionConfig.getHeight(),
|
|
1200
|
+
sessionConfig.getX(),
|
|
1201
|
+
sessionConfig.getY()
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
public String getAspectRatio() {
|
|
1207
|
+
if (sessionConfig != null) {
|
|
1208
|
+
return sessionConfig.getAspectRatio();
|
|
1209
|
+
}
|
|
1210
|
+
return "4:3";
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
public String getGridMode() {
|
|
1214
|
+
if (sessionConfig != null) {
|
|
1215
|
+
return sessionConfig.getGridMode();
|
|
1216
|
+
}
|
|
1217
|
+
return "none";
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
public void setAspectRatio(String aspectRatio) {
|
|
1221
|
+
setAspectRatio(aspectRatio, null, null);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
public void setAspectRatio(String aspectRatio, Float x, Float y) {
|
|
1225
|
+
setAspectRatio(aspectRatio, x, y, null);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
public void setAspectRatio(String aspectRatio, Float x, Float y, Runnable callback) {
|
|
1229
|
+
if (sessionConfig == null) {
|
|
1230
|
+
if (callback != null) callback.run();
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
String currentAspectRatio = sessionConfig.getAspectRatio();
|
|
1235
|
+
|
|
1236
|
+
// Don't restart camera if aspect ratio hasn't changed and no position specified
|
|
1237
|
+
if (aspectRatio != null && aspectRatio.equals(currentAspectRatio) && x == null && y == null) {
|
|
1238
|
+
Log.d(TAG, "setAspectRatio: Aspect ratio " + aspectRatio + " is already set and no position specified, skipping");
|
|
1239
|
+
if (callback != null) callback.run();
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
String currentGridMode = sessionConfig.getGridMode();
|
|
1244
|
+
Log.d(TAG, "setAspectRatio: Changing from " + currentAspectRatio + " to " + aspectRatio +
|
|
1245
|
+
(x != null && y != null ? " at position (" + x + ", " + y + ")" : " with auto-centering") +
|
|
1246
|
+
", preserving grid mode: " + currentGridMode);
|
|
1247
|
+
|
|
1248
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
1249
|
+
sessionConfig.getDeviceId(),
|
|
1250
|
+
sessionConfig.getPosition(),
|
|
1251
|
+
sessionConfig.getX(),
|
|
1252
|
+
sessionConfig.getY(),
|
|
1253
|
+
sessionConfig.getWidth(),
|
|
1254
|
+
sessionConfig.getHeight(),
|
|
1255
|
+
sessionConfig.getPaddingBottom(),
|
|
1256
|
+
sessionConfig.getToBack(),
|
|
1257
|
+
sessionConfig.getStoreToFile(),
|
|
1258
|
+
sessionConfig.getEnableOpacity(),
|
|
1259
|
+
sessionConfig.getEnableZoom(),
|
|
1260
|
+
sessionConfig.getDisableExifHeaderStripping(),
|
|
1261
|
+
sessionConfig.getDisableAudio(),
|
|
1262
|
+
sessionConfig.getZoomFactor(),
|
|
1263
|
+
aspectRatio,
|
|
1264
|
+
currentGridMode
|
|
1265
|
+
);
|
|
1266
|
+
|
|
1267
|
+
// Update layout and rebind camera with new aspect ratio
|
|
1268
|
+
if (isRunning && previewContainer != null) {
|
|
1269
|
+
mainExecutor.execute(() -> {
|
|
1270
|
+
// First update the UI layout
|
|
1271
|
+
updatePreviewLayoutForAspectRatio(aspectRatio, x, y);
|
|
1272
|
+
|
|
1273
|
+
// Then rebind the camera with new aspect ratio configuration
|
|
1274
|
+
Log.d(TAG, "setAspectRatio: Rebinding camera with new aspect ratio: " + aspectRatio);
|
|
1275
|
+
bindCameraUseCases();
|
|
1276
|
+
|
|
1277
|
+
// Preserve grid mode and wait for completion
|
|
1278
|
+
if (gridOverlayView != null) {
|
|
1279
|
+
gridOverlayView.post(() -> {
|
|
1280
|
+
Log.d(TAG, "setAspectRatio: Re-applying grid mode: " + currentGridMode);
|
|
1281
|
+
gridOverlayView.setGridMode(currentGridMode);
|
|
1282
|
+
|
|
1283
|
+
// Wait one more frame for grid to be applied, then call callback
|
|
1284
|
+
if (callback != null) {
|
|
1285
|
+
gridOverlayView.post(callback);
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
} else {
|
|
1289
|
+
// No grid overlay, wait one frame for layout completion then call callback
|
|
1290
|
+
if (callback != null) {
|
|
1291
|
+
previewContainer.post(callback);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
} else {
|
|
1296
|
+
if (callback != null) callback.run();
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
public void setGridMode(String gridMode) {
|
|
1301
|
+
if (sessionConfig != null) {
|
|
1302
|
+
Log.d(TAG, "setGridMode: Changing grid mode to: " + gridMode);
|
|
1303
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
1304
|
+
sessionConfig.getDeviceId(),
|
|
1305
|
+
sessionConfig.getPosition(),
|
|
1306
|
+
sessionConfig.getX(),
|
|
1307
|
+
sessionConfig.getY(),
|
|
1308
|
+
sessionConfig.getWidth(),
|
|
1309
|
+
sessionConfig.getHeight(),
|
|
1310
|
+
sessionConfig.getPaddingBottom(),
|
|
1311
|
+
sessionConfig.getToBack(),
|
|
1312
|
+
sessionConfig.getStoreToFile(),
|
|
1313
|
+
sessionConfig.getEnableOpacity(),
|
|
1314
|
+
sessionConfig.getEnableZoom(),
|
|
1315
|
+
sessionConfig.getDisableExifHeaderStripping(),
|
|
1316
|
+
sessionConfig.getDisableAudio(),
|
|
1317
|
+
sessionConfig.getZoomFactor(),
|
|
1318
|
+
sessionConfig.getAspectRatio(),
|
|
1319
|
+
gridMode
|
|
1320
|
+
);
|
|
1321
|
+
|
|
1322
|
+
// Update the grid overlay immediately
|
|
1323
|
+
if (gridOverlayView != null) {
|
|
1324
|
+
gridOverlayView.post(() -> {
|
|
1325
|
+
Log.d(TAG, "setGridMode: Applying grid mode to overlay: " + gridMode);
|
|
1326
|
+
gridOverlayView.setGridMode(gridMode);
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
public int getPreviewX() {
|
|
1333
|
+
if (previewContainer == null) return 0;
|
|
1334
|
+
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
1335
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1336
|
+
// Return position relative to WebView content (subtract insets)
|
|
1337
|
+
int margin = ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
|
|
1338
|
+
int leftInset = getWebViewLeftInset();
|
|
1339
|
+
int result = margin - leftInset;
|
|
1340
|
+
Log.d(TAG, "getPreviewX: leftMargin=" + margin + ", leftInset=" + leftInset + ", result=" + result);
|
|
1341
|
+
return result;
|
|
1342
|
+
}
|
|
1343
|
+
return previewContainer.getLeft();
|
|
1344
|
+
}
|
|
1345
|
+
public int getPreviewY() {
|
|
1346
|
+
if (previewContainer == null) return 0;
|
|
1347
|
+
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
1348
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1349
|
+
// Return position relative to WebView content (subtract insets)
|
|
1350
|
+
int margin = ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
|
|
1351
|
+
int topInset = getWebViewTopInset();
|
|
1352
|
+
int result = margin - topInset;
|
|
1353
|
+
Log.d(TAG, "getPreviewY: topMargin=" + margin + ", topInset=" + topInset + ", result=" + result);
|
|
1354
|
+
return result;
|
|
1355
|
+
}
|
|
1356
|
+
return previewContainer.getTop();
|
|
1357
|
+
}
|
|
1358
|
+
public int getPreviewWidth() {
|
|
1359
|
+
return previewContainer != null ? previewContainer.getWidth() : 0;
|
|
1360
|
+
}
|
|
1361
|
+
public int getPreviewHeight() {
|
|
1362
|
+
return previewContainer != null ? previewContainer.getHeight() : 0;
|
|
1363
|
+
}
|
|
1364
|
+
public void setPreviewSize(int x, int y, int width, int height) {
|
|
1365
|
+
setPreviewSize(x, y, width, height, null);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
public void setPreviewSize(int x, int y, int width, int height, Runnable callback) {
|
|
1369
|
+
if (previewContainer == null) {
|
|
1370
|
+
if (callback != null) callback.run();
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// Ensure this runs on the main UI thread
|
|
1375
|
+
mainExecutor.execute(() -> {
|
|
1376
|
+
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
1377
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1378
|
+
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) layoutParams;
|
|
1379
|
+
|
|
1380
|
+
// Only add insets for positioning coordinates, not for full-screen sizes
|
|
1381
|
+
int webViewTopInset = getWebViewTopInset();
|
|
1382
|
+
int webViewLeftInset = getWebViewLeftInset();
|
|
1383
|
+
|
|
1384
|
+
// Handle positioning - preserve current values if new values are not specified (negative)
|
|
1385
|
+
if (x >= 0) {
|
|
1386
|
+
// Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
|
|
1387
|
+
if (x == 0 && y == 0) {
|
|
1388
|
+
params.leftMargin = x;
|
|
1389
|
+
Log.d(TAG, "setPreviewSize: Full-screen mode - keeping x=0 without insets");
|
|
1390
|
+
} else {
|
|
1391
|
+
params.leftMargin = x + webViewLeftInset;
|
|
1392
|
+
Log.d(TAG, "setPreviewSize: Positioned mode - x=" + x + " + inset=" + webViewLeftInset + " = " + (x + webViewLeftInset));
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (y >= 0) {
|
|
1396
|
+
// Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
|
|
1397
|
+
if (x == 0 && y == 0) {
|
|
1398
|
+
params.topMargin = y;
|
|
1399
|
+
Log.d(TAG, "setPreviewSize: Full-screen mode - keeping y=0 without insets");
|
|
1400
|
+
} else {
|
|
1401
|
+
params.topMargin = y + webViewTopInset;
|
|
1402
|
+
Log.d(TAG, "setPreviewSize: Positioned mode - y=" + y + " + inset=" + webViewTopInset + " = " + (y + webViewTopInset));
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (width > 0) params.width = width;
|
|
1406
|
+
if (height > 0) params.height = height;
|
|
1407
|
+
|
|
1408
|
+
previewContainer.setLayoutParams(params);
|
|
1409
|
+
previewContainer.requestLayout();
|
|
1410
|
+
|
|
1411
|
+
Log.d(TAG, "setPreviewSize: Updated to " + params.width + "x" + params.height + " at (" + params.leftMargin + "," + params.topMargin + ")");
|
|
1412
|
+
|
|
1413
|
+
// Update session config to reflect actual layout
|
|
1414
|
+
if (sessionConfig != null) {
|
|
1415
|
+
String currentAspectRatio = sessionConfig.getAspectRatio();
|
|
1416
|
+
|
|
1417
|
+
// Calculate aspect ratio from actual dimensions if both width and height are provided
|
|
1418
|
+
String calculatedAspectRatio = currentAspectRatio;
|
|
1419
|
+
if (params.width > 0 && params.height > 0) {
|
|
1420
|
+
// Always use larger dimension / smaller dimension for consistent comparison
|
|
1421
|
+
float ratio = Math.max(params.width, params.height) / (float) Math.min(params.width, params.height);
|
|
1422
|
+
// Standard ratios: 16:9 ≈ 1.778, 4:3 ≈ 1.333
|
|
1423
|
+
float ratio16_9 = 16f / 9f; // 1.778
|
|
1424
|
+
float ratio4_3 = 4f / 3f; // 1.333
|
|
1425
|
+
|
|
1426
|
+
// Determine closest standard aspect ratio
|
|
1427
|
+
if (Math.abs(ratio - ratio16_9) < Math.abs(ratio - ratio4_3)) {
|
|
1428
|
+
calculatedAspectRatio = "16:9";
|
|
1429
|
+
} else {
|
|
1430
|
+
calculatedAspectRatio = "4:3";
|
|
1431
|
+
}
|
|
1432
|
+
Log.d(TAG, "setPreviewSize: Calculated aspect ratio from " + params.width + "x" + params.height + " = " + calculatedAspectRatio + " (normalized ratio=" + ratio + ")");
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
1436
|
+
sessionConfig.getDeviceId(),
|
|
1437
|
+
sessionConfig.getPosition(),
|
|
1438
|
+
params.leftMargin,
|
|
1439
|
+
params.topMargin,
|
|
1440
|
+
params.width,
|
|
1441
|
+
params.height,
|
|
1442
|
+
sessionConfig.getPaddingBottom(),
|
|
1443
|
+
sessionConfig.getToBack(),
|
|
1444
|
+
sessionConfig.getStoreToFile(),
|
|
1445
|
+
sessionConfig.getEnableOpacity(),
|
|
1446
|
+
sessionConfig.getEnableZoom(),
|
|
1447
|
+
sessionConfig.getDisableExifHeaderStripping(),
|
|
1448
|
+
sessionConfig.getDisableAudio(),
|
|
1449
|
+
sessionConfig.getZoomFactor(),
|
|
1450
|
+
calculatedAspectRatio,
|
|
1451
|
+
sessionConfig.getGridMode()
|
|
1452
|
+
);
|
|
1453
|
+
|
|
1454
|
+
// If aspect ratio changed due to size update, rebind camera
|
|
1455
|
+
if (isRunning && !Objects.equals(currentAspectRatio, calculatedAspectRatio)) {
|
|
1456
|
+
Log.d(TAG, "setPreviewSize: Aspect ratio changed from " + currentAspectRatio + " to " + calculatedAspectRatio + ", rebinding camera");
|
|
1457
|
+
bindCameraUseCases();
|
|
1458
|
+
|
|
1459
|
+
// Wait for camera rebinding to complete, then call callback
|
|
1460
|
+
if (callback != null) {
|
|
1461
|
+
previewContainer.post(() -> previewContainer.post(callback));
|
|
1462
|
+
}
|
|
1463
|
+
} else {
|
|
1464
|
+
// No camera rebinding needed, wait for layout to complete then call callback
|
|
1465
|
+
if (callback != null) {
|
|
1466
|
+
previewContainer.post(callback);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
} else {
|
|
1470
|
+
// No sessionConfig, just wait for layout then call callback
|
|
1471
|
+
if (callback != null) {
|
|
1472
|
+
previewContainer.post(callback);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
} else {
|
|
1476
|
+
Log.w(TAG, "setPreviewSize: Cannot set margins on layout params of type " + layoutParams.getClass().getSimpleName());
|
|
1477
|
+
// Fallback: just set width and height if specified
|
|
1478
|
+
if (width > 0) layoutParams.width = width;
|
|
1479
|
+
if (height > 0) layoutParams.height = height;
|
|
1480
|
+
previewContainer.setLayoutParams(layoutParams);
|
|
1481
|
+
previewContainer.requestLayout();
|
|
1482
|
+
|
|
1483
|
+
// Wait for layout then call callback
|
|
1484
|
+
if (callback != null) {
|
|
1485
|
+
previewContainer.post(callback);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private void updatePreviewLayoutForAspectRatio(String aspectRatio) {
|
|
1492
|
+
updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
private void updatePreviewLayoutForAspectRatio(String aspectRatio, Float x, Float y) {
|
|
1496
|
+
if (previewContainer == null || aspectRatio == null) return;
|
|
1497
|
+
|
|
1498
|
+
// Parse aspect ratio
|
|
1499
|
+
String[] ratios = aspectRatio.split(":");
|
|
1500
|
+
if (ratios.length != 2) return;
|
|
1501
|
+
|
|
1502
|
+
try {
|
|
1503
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
1504
|
+
float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
|
|
1505
|
+
|
|
1506
|
+
// Get available space from webview dimensions
|
|
1507
|
+
int availableWidth = webView.getWidth();
|
|
1508
|
+
int availableHeight = webView.getHeight();
|
|
1509
|
+
|
|
1510
|
+
// Calculate position and size
|
|
1511
|
+
int finalX, finalY, finalWidth, finalHeight;
|
|
1512
|
+
|
|
1513
|
+
if (x != null && y != null) {
|
|
1514
|
+
// Account for WebView insets from edge-to-edge support
|
|
1515
|
+
int webViewTopInset = getWebViewTopInset();
|
|
1516
|
+
int webViewLeftInset = getWebViewLeftInset();
|
|
1517
|
+
|
|
1518
|
+
// Use provided coordinates with boundary checking, adjusted for insets
|
|
1519
|
+
finalX = Math.max(0, Math.min(x.intValue() + webViewLeftInset, availableWidth));
|
|
1520
|
+
finalY = Math.max(0, Math.min(y.intValue() + webViewTopInset, availableHeight));
|
|
1521
|
+
|
|
1522
|
+
// Calculate maximum available space from the given position
|
|
1523
|
+
int maxWidth = availableWidth - finalX;
|
|
1524
|
+
int maxHeight = availableHeight - finalY;
|
|
1525
|
+
|
|
1526
|
+
// Calculate optimal size while maintaining aspect ratio within available space
|
|
1527
|
+
finalWidth = maxWidth;
|
|
1528
|
+
finalHeight = (int) (maxWidth / ratio);
|
|
1529
|
+
|
|
1530
|
+
if (finalHeight > maxHeight) {
|
|
1531
|
+
// Height constraint is tighter, fit by height
|
|
1532
|
+
finalHeight = maxHeight;
|
|
1533
|
+
finalWidth = (int) (maxHeight * ratio);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Ensure final position stays within bounds
|
|
1537
|
+
finalX = Math.max(0, Math.min(finalX, availableWidth - finalWidth));
|
|
1538
|
+
finalY = Math.max(0, Math.min(finalY, availableHeight - finalHeight));
|
|
1539
|
+
} else {
|
|
1540
|
+
// Auto-center the view
|
|
1541
|
+
// Calculate size based on aspect ratio, using a reasonable base size
|
|
1542
|
+
// Use 80% of available space to ensure aspect ratio differences are visible
|
|
1543
|
+
int maxAvailableWidth = (int) (availableWidth * 0.8);
|
|
1544
|
+
int maxAvailableHeight = (int) (availableHeight * 0.8);
|
|
1545
|
+
|
|
1546
|
+
// Start with width-based calculation
|
|
1547
|
+
finalWidth = maxAvailableWidth;
|
|
1548
|
+
finalHeight = (int) (finalWidth / ratio);
|
|
1549
|
+
|
|
1550
|
+
// If height exceeds available space, use height-based calculation
|
|
1551
|
+
if (finalHeight > maxAvailableHeight) {
|
|
1552
|
+
finalHeight = maxAvailableHeight;
|
|
1553
|
+
finalWidth = (int) (finalHeight * ratio);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// Center the view
|
|
1557
|
+
finalX = (availableWidth - finalWidth) / 2;
|
|
1558
|
+
finalY = (availableHeight - finalHeight) / 2;
|
|
1559
|
+
|
|
1560
|
+
Log.d(TAG, "updatePreviewLayoutForAspectRatio: Auto-center mode - ratio=" + ratio +
|
|
1561
|
+
", calculated size=" + finalWidth + "x" + finalHeight +
|
|
1562
|
+
", available=" + availableWidth + "x" + availableHeight);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Update layout params
|
|
1566
|
+
ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
|
|
1567
|
+
if (currentParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1568
|
+
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) currentParams;
|
|
1569
|
+
params.width = finalWidth;
|
|
1570
|
+
params.height = finalHeight;
|
|
1571
|
+
params.leftMargin = finalX;
|
|
1572
|
+
params.topMargin = finalY;
|
|
1573
|
+
previewContainer.setLayoutParams(params);
|
|
1574
|
+
previewContainer.requestLayout();
|
|
1575
|
+
Log.d(TAG, "updatePreviewLayoutForAspectRatio: Updated to " + finalWidth + "x" + finalHeight + " at (" + finalX + "," + finalY + ")");
|
|
1576
|
+
}
|
|
1577
|
+
} catch (NumberFormatException e) {
|
|
1578
|
+
Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
private void updatePreviewLayout() {
|
|
1583
|
+
if (previewContainer == null || sessionConfig == null) return;
|
|
1584
|
+
|
|
1585
|
+
String aspectRatio = sessionConfig.getAspectRatio();
|
|
1586
|
+
if (aspectRatio == null) return;
|
|
1587
|
+
|
|
1588
|
+
updatePreviewLayoutForAspectRatio(aspectRatio);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
private int getWebViewTopInset() {
|
|
1592
|
+
try {
|
|
1593
|
+
if (webView != null) {
|
|
1594
|
+
ViewGroup.LayoutParams layoutParams = webView.getLayoutParams();
|
|
1595
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1596
|
+
return ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
} catch (Exception e) {
|
|
1600
|
+
Log.w(TAG, "Failed to get WebView top inset", e);
|
|
1601
|
+
}
|
|
1602
|
+
return 0;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
private int getWebViewLeftInset() {
|
|
1606
|
+
try {
|
|
1607
|
+
if (webView != null) {
|
|
1608
|
+
ViewGroup.LayoutParams layoutParams = webView.getLayoutParams();
|
|
1609
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1610
|
+
return ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
} catch (Exception e) {
|
|
1614
|
+
Log.w(TAG, "Failed to get WebView left inset", e);
|
|
1615
|
+
}
|
|
1616
|
+
return 0;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Get the current preview position and size in DP units (without insets)
|
|
1621
|
+
*/
|
|
1622
|
+
public int[] getCurrentPreviewBounds() {
|
|
1623
|
+
if (previewContainer == null) {
|
|
1624
|
+
return new int[]{0, 0, 0, 0}; // x, y, width, height
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
1628
|
+
int x = 0, y = 0, width = 0, height = 0;
|
|
1629
|
+
|
|
1630
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
1631
|
+
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) layoutParams;
|
|
1632
|
+
|
|
1633
|
+
// Remove insets to get original coordinates in DP
|
|
1634
|
+
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
|
1635
|
+
float pixelRatio = metrics.density;
|
|
1636
|
+
|
|
1637
|
+
int webViewTopInset = getWebViewTopInset();
|
|
1638
|
+
int webViewLeftInset = getWebViewLeftInset();
|
|
1639
|
+
|
|
1640
|
+
x = Math.max(0, (int) ((params.leftMargin - webViewLeftInset) / pixelRatio));
|
|
1641
|
+
y = Math.max(0, (int) ((params.topMargin - webViewTopInset) / pixelRatio));
|
|
1642
|
+
width = (int) (params.width / pixelRatio);
|
|
1643
|
+
height = (int) (params.height / pixelRatio);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
return new int[]{x, y, width, height};
|
|
1647
|
+
}
|
|
848
1648
|
}
|