@capgo/camera-preview 7.4.0-beta.2 → 7.4.0-beta.21
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 +218 -35
- 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 +1 -4
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +759 -83
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +2813 -805
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +112 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +161 -59
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
- package/dist/docs.json +333 -29
- package/dist/esm/definitions.d.ts +156 -13
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +52 -3
- package/dist/esm/web.js +592 -95
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +590 -95
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +590 -95
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +907 -222
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +986 -250
- package/package.json +2 -2
|
@@ -1,968 +1,2976 @@
|
|
|
1
1
|
package com.ahm.capacitor.camera.preview;
|
|
2
2
|
|
|
3
3
|
import android.content.Context;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
import android.graphics.BitmapFactory;
|
|
6
|
+
import android.graphics.Color;
|
|
7
|
+
import android.graphics.Rect;
|
|
8
|
+
import android.graphics.drawable.GradientDrawable;
|
|
4
9
|
import android.hardware.camera2.CameraAccessException;
|
|
10
|
+
import android.hardware.camera2.CameraCharacteristics;
|
|
5
11
|
import android.hardware.camera2.CameraManager;
|
|
12
|
+
import android.location.Location;
|
|
13
|
+
import android.media.MediaScannerConnection;
|
|
6
14
|
import android.os.Build;
|
|
15
|
+
import android.os.Environment;
|
|
7
16
|
import android.util.Base64;
|
|
17
|
+
import android.util.DisplayMetrics;
|
|
8
18
|
import android.util.Log;
|
|
9
19
|
import android.util.Size;
|
|
20
|
+
import android.view.MotionEvent;
|
|
21
|
+
import android.view.View;
|
|
10
22
|
import android.view.ViewGroup;
|
|
23
|
+
import android.view.animation.AlphaAnimation;
|
|
24
|
+
import android.view.animation.Animation;
|
|
25
|
+
import android.view.animation.AnimationSet;
|
|
26
|
+
import android.view.animation.AnimationUtils;
|
|
27
|
+
import android.view.animation.ScaleAnimation;
|
|
28
|
+
import android.webkit.WebView;
|
|
11
29
|
import android.webkit.WebView;
|
|
30
|
+
import android.widget.FrameLayout;
|
|
31
|
+
import android.widget.FrameLayout;
|
|
12
32
|
import androidx.annotation.NonNull;
|
|
13
33
|
import androidx.annotation.OptIn;
|
|
34
|
+
import androidx.camera.camera2.interop.Camera2CameraInfo;
|
|
35
|
+
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
|
|
36
|
+
import androidx.camera.core.AspectRatio;
|
|
14
37
|
import androidx.camera.core.Camera;
|
|
15
38
|
import androidx.camera.core.CameraInfo;
|
|
16
39
|
import androidx.camera.core.CameraSelector;
|
|
40
|
+
import androidx.camera.core.FocusMeteringAction;
|
|
41
|
+
import androidx.camera.core.FocusMeteringResult;
|
|
17
42
|
import androidx.camera.core.ImageCapture;
|
|
18
43
|
import androidx.camera.core.ImageCaptureException;
|
|
19
44
|
import androidx.camera.core.ImageProxy;
|
|
45
|
+
import androidx.camera.core.MeteringPoint;
|
|
46
|
+
import androidx.camera.core.MeteringPointFactory;
|
|
20
47
|
import androidx.camera.core.Preview;
|
|
48
|
+
import androidx.camera.core.ResolutionInfo;
|
|
49
|
+
import androidx.camera.core.ZoomState;
|
|
50
|
+
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
|
|
21
51
|
import androidx.camera.core.resolutionselector.ResolutionSelector;
|
|
22
52
|
import androidx.camera.core.resolutionselector.ResolutionStrategy;
|
|
23
53
|
import androidx.camera.lifecycle.ProcessCameraProvider;
|
|
24
54
|
import androidx.camera.view.PreviewView;
|
|
25
55
|
import androidx.core.content.ContextCompat;
|
|
56
|
+
import androidx.exifinterface.media.ExifInterface;
|
|
26
57
|
import androidx.lifecycle.Lifecycle;
|
|
58
|
+
import androidx.lifecycle.LifecycleObserver;
|
|
27
59
|
import androidx.lifecycle.LifecycleOwner;
|
|
28
60
|
import androidx.lifecycle.LifecycleRegistry;
|
|
29
61
|
import com.ahm.capacitor.camera.preview.model.CameraSessionConfiguration;
|
|
30
62
|
import com.ahm.capacitor.camera.preview.model.LensInfo;
|
|
31
63
|
import com.ahm.capacitor.camera.preview.model.ZoomFactors;
|
|
32
64
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
65
|
+
import java.io.ByteArrayOutputStream;
|
|
66
|
+
import java.io.File;
|
|
67
|
+
import java.io.FileOutputStream;
|
|
68
|
+
import java.io.IOException;
|
|
33
69
|
import java.nio.ByteBuffer;
|
|
70
|
+
import java.text.SimpleDateFormat;
|
|
71
|
+
import java.util.ArrayList;
|
|
34
72
|
import java.util.Arrays;
|
|
35
73
|
import java.util.Collections;
|
|
36
74
|
import java.util.List;
|
|
37
|
-
import java.util.
|
|
75
|
+
import java.util.Locale;
|
|
38
76
|
import java.util.Objects;
|
|
77
|
+
import java.util.Set;
|
|
39
78
|
import java.util.concurrent.Executor;
|
|
40
79
|
import java.util.concurrent.ExecutorService;
|
|
41
80
|
import java.util.concurrent.Executors;
|
|
42
|
-
import
|
|
43
|
-
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
|
|
44
|
-
import android.hardware.camera2.CameraCharacteristics;
|
|
45
|
-
import java.util.Set;
|
|
46
|
-
import androidx.camera.core.ZoomState;
|
|
47
|
-
import androidx.camera.core.ResolutionInfo;
|
|
48
|
-
import android.content.Intent;
|
|
49
|
-
import android.net.Uri;
|
|
50
|
-
import android.os.Environment;
|
|
51
|
-
import java.io.File;
|
|
52
|
-
import java.io.FileOutputStream;
|
|
53
|
-
import java.io.IOException;
|
|
54
|
-
import java.text.SimpleDateFormat;
|
|
55
|
-
import java.util.Locale;
|
|
56
|
-
import androidx.exifinterface.media.ExifInterface;
|
|
81
|
+
import java.util.concurrent.TimeUnit;
|
|
57
82
|
import org.json.JSONObject;
|
|
58
|
-
import java.nio.file.Files;
|
|
59
83
|
|
|
60
|
-
public class CameraXView implements LifecycleOwner {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
85
|
+
|
|
86
|
+
private static final String TAG = "CameraPreview CameraXView";
|
|
87
|
+
|
|
88
|
+
public interface CameraXViewListener {
|
|
89
|
+
void onPictureTaken(String base64, JSONObject exif);
|
|
90
|
+
void onPictureTakenError(String message);
|
|
91
|
+
void onSampleTaken(String result);
|
|
92
|
+
void onSampleTakenError(String message);
|
|
93
|
+
void onCameraStarted(int width, int height, int x, int y);
|
|
94
|
+
void onCameraStartError(String message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private ProcessCameraProvider cameraProvider;
|
|
98
|
+
private Camera camera;
|
|
99
|
+
private ImageCapture imageCapture;
|
|
100
|
+
private ImageCapture sampleImageCapture;
|
|
101
|
+
private PreviewView previewView;
|
|
102
|
+
private GridOverlayView gridOverlayView;
|
|
103
|
+
private FrameLayout previewContainer;
|
|
104
|
+
private View focusIndicatorView;
|
|
105
|
+
private CameraSelector currentCameraSelector;
|
|
106
|
+
private String currentDeviceId;
|
|
107
|
+
private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
|
|
108
|
+
private CameraSessionConfiguration sessionConfig;
|
|
109
|
+
private CameraXViewListener listener;
|
|
110
|
+
private final Context context;
|
|
111
|
+
private final WebView webView;
|
|
112
|
+
private final LifecycleRegistry lifecycleRegistry;
|
|
113
|
+
private final Executor mainExecutor;
|
|
114
|
+
private ExecutorService cameraExecutor;
|
|
115
|
+
private boolean isRunning = false;
|
|
116
|
+
private Size currentPreviewResolution = null;
|
|
117
|
+
private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
|
|
118
|
+
|
|
119
|
+
public CameraXView(Context context, WebView webView) {
|
|
120
|
+
this.context = context;
|
|
121
|
+
this.webView = webView;
|
|
122
|
+
this.lifecycleRegistry = new LifecycleRegistry(this);
|
|
123
|
+
this.mainExecutor = ContextCompat.getMainExecutor(context);
|
|
124
|
+
|
|
125
|
+
mainExecutor.execute(() ->
|
|
126
|
+
lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@NonNull
|
|
131
|
+
@Override
|
|
132
|
+
public Lifecycle getLifecycle() {
|
|
133
|
+
return lifecycleRegistry;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public void setListener(CameraXViewListener listener) {
|
|
137
|
+
this.listener = listener;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public boolean isRunning() {
|
|
141
|
+
return isRunning;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private void saveImageToGallery(byte[] data) {
|
|
145
|
+
try {
|
|
146
|
+
// Detect image format from byte array header
|
|
147
|
+
String extension = ".jpg";
|
|
148
|
+
String mimeType = "image/jpeg";
|
|
149
|
+
|
|
150
|
+
if (data.length >= 8) {
|
|
151
|
+
// Check for PNG signature (89 50 4E 47 0D 0A 1A 0A)
|
|
152
|
+
if (
|
|
153
|
+
data[0] == (byte) 0x89 &&
|
|
154
|
+
data[1] == 0x50 &&
|
|
155
|
+
data[2] == 0x4E &&
|
|
156
|
+
data[3] == 0x47
|
|
157
|
+
) {
|
|
158
|
+
extension = ".png";
|
|
159
|
+
mimeType = "image/png";
|
|
160
|
+
}
|
|
161
|
+
// Check for JPEG signature (FF D8 FF)
|
|
162
|
+
else if (
|
|
163
|
+
data[0] == (byte) 0xFF &&
|
|
164
|
+
data[1] == (byte) 0xD8 &&
|
|
165
|
+
data[2] == (byte) 0xFF
|
|
166
|
+
) {
|
|
167
|
+
extension = ".jpg";
|
|
168
|
+
mimeType = "image/jpeg";
|
|
169
|
+
}
|
|
170
|
+
// Check for WebP signature (RIFF ... WEBP)
|
|
171
|
+
else if (
|
|
172
|
+
data[0] == 0x52 &&
|
|
173
|
+
data[1] == 0x49 &&
|
|
174
|
+
data[2] == 0x46 &&
|
|
175
|
+
data[3] == 0x46 &&
|
|
176
|
+
data.length >= 12 &&
|
|
177
|
+
data[8] == 0x57 &&
|
|
178
|
+
data[9] == 0x45 &&
|
|
179
|
+
data[10] == 0x42 &&
|
|
180
|
+
data[11] == 0x50
|
|
181
|
+
) {
|
|
182
|
+
extension = ".webp";
|
|
183
|
+
mimeType = "image/webp";
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
File photo = new File(
|
|
188
|
+
Environment.getExternalStoragePublicDirectory(
|
|
189
|
+
Environment.DIRECTORY_PICTURES
|
|
190
|
+
),
|
|
191
|
+
"IMG_" +
|
|
192
|
+
new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(
|
|
193
|
+
new java.util.Date()
|
|
194
|
+
) +
|
|
195
|
+
extension
|
|
196
|
+
);
|
|
197
|
+
FileOutputStream fos = new FileOutputStream(photo);
|
|
198
|
+
fos.write(data);
|
|
199
|
+
fos.close();
|
|
200
|
+
|
|
201
|
+
// Notify the gallery of the new image
|
|
202
|
+
MediaScannerConnection.scanFile(
|
|
203
|
+
this.context,
|
|
204
|
+
new String[] { photo.getAbsolutePath() },
|
|
205
|
+
new String[] { mimeType },
|
|
206
|
+
null
|
|
207
|
+
);
|
|
208
|
+
} catch (IOException e) {
|
|
209
|
+
Log.e(TAG, "Error saving image to gallery", e);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public void startSession(CameraSessionConfiguration config) {
|
|
214
|
+
this.sessionConfig = config;
|
|
215
|
+
cameraExecutor = Executors.newSingleThreadExecutor();
|
|
216
|
+
mainExecutor.execute(() -> {
|
|
217
|
+
lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
|
|
218
|
+
setupCamera();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
public void stopSession() {
|
|
223
|
+
isRunning = false;
|
|
224
|
+
// Cancel any ongoing focus operation when stopping session
|
|
225
|
+
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
226
|
+
currentFocusFuture.cancel(true);
|
|
227
|
+
}
|
|
228
|
+
currentFocusFuture = null;
|
|
229
|
+
|
|
230
|
+
mainExecutor.execute(() -> {
|
|
231
|
+
if (cameraProvider != null) {
|
|
232
|
+
cameraProvider.unbindAll();
|
|
233
|
+
}
|
|
234
|
+
lifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
|
|
235
|
+
if (cameraExecutor != null) {
|
|
236
|
+
cameraExecutor.shutdownNow();
|
|
237
|
+
}
|
|
238
|
+
removePreviewView();
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private void setupCamera() {
|
|
243
|
+
ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
|
|
244
|
+
ProcessCameraProvider.getInstance(context);
|
|
245
|
+
cameraProviderFuture.addListener(
|
|
246
|
+
() -> {
|
|
247
|
+
try {
|
|
248
|
+
cameraProvider = cameraProviderFuture.get();
|
|
249
|
+
setupPreviewView();
|
|
250
|
+
bindCameraUseCases();
|
|
251
|
+
} catch (Exception e) {
|
|
252
|
+
if (listener != null) {
|
|
253
|
+
listener.onCameraStartError(
|
|
254
|
+
"Error initializing camera: " + e.getMessage()
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
mainExecutor
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private void setupPreviewView() {
|
|
264
|
+
if (previewView != null) {
|
|
265
|
+
removePreviewView();
|
|
266
|
+
}
|
|
267
|
+
if (sessionConfig.isToBack()) {
|
|
268
|
+
webView.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
|
70
269
|
}
|
|
71
270
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
271
|
+
// Create a container to hold both the preview and grid overlay
|
|
272
|
+
previewContainer = new FrameLayout(context);
|
|
273
|
+
// Ensure container can receive touch events
|
|
274
|
+
previewContainer.setClickable(true);
|
|
275
|
+
previewContainer.setFocusable(true);
|
|
276
|
+
|
|
277
|
+
// Create and setup the preview view
|
|
278
|
+
previewView = new PreviewView(context);
|
|
279
|
+
previewView.setScaleType(PreviewView.ScaleType.FIT_CENTER);
|
|
280
|
+
// Also make preview view touchable as backup
|
|
281
|
+
previewView.setClickable(true);
|
|
282
|
+
previewView.setFocusable(true);
|
|
283
|
+
|
|
284
|
+
// Add touch listener to both container and preview view for maximum compatibility
|
|
285
|
+
View.OnTouchListener touchListener = new View.OnTouchListener() {
|
|
286
|
+
@Override
|
|
287
|
+
public boolean onTouch(View v, MotionEvent event) {
|
|
288
|
+
Log.d(
|
|
289
|
+
TAG,
|
|
290
|
+
"onTouch: " +
|
|
291
|
+
v.getClass().getSimpleName() +
|
|
292
|
+
" received touch event: " +
|
|
293
|
+
event.getAction() +
|
|
294
|
+
" at (" +
|
|
295
|
+
event.getX() +
|
|
296
|
+
", " +
|
|
297
|
+
event.getY() +
|
|
298
|
+
")"
|
|
299
|
+
);
|
|
88
300
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
301
|
+
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
|
302
|
+
float x = event.getX() / v.getWidth();
|
|
303
|
+
float y = event.getY() / v.getHeight();
|
|
304
|
+
|
|
305
|
+
Log.d(
|
|
306
|
+
TAG,
|
|
307
|
+
"onTouch: Touch detected at raw coords (" +
|
|
308
|
+
event.getX() +
|
|
309
|
+
", " +
|
|
310
|
+
event.getY() +
|
|
311
|
+
"), view size: " +
|
|
312
|
+
v.getWidth() +
|
|
313
|
+
"x" +
|
|
314
|
+
v.getHeight() +
|
|
315
|
+
", normalized: (" +
|
|
316
|
+
x +
|
|
317
|
+
", " +
|
|
318
|
+
y +
|
|
319
|
+
")"
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
// Trigger focus with indicator
|
|
324
|
+
setFocus(x, y);
|
|
325
|
+
} catch (Exception e) {
|
|
326
|
+
Log.e(TAG, "Error during tap-to-focus: " + e.getMessage(), e);
|
|
327
|
+
}
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
94
333
|
|
|
95
|
-
|
|
96
|
-
|
|
334
|
+
previewContainer.setOnTouchListener(touchListener);
|
|
335
|
+
previewView.setOnTouchListener(touchListener);
|
|
336
|
+
|
|
337
|
+
previewContainer.addView(
|
|
338
|
+
previewView,
|
|
339
|
+
new FrameLayout.LayoutParams(
|
|
340
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
341
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
342
|
+
)
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// Create and setup the grid overlay
|
|
346
|
+
gridOverlayView = new GridOverlayView(context);
|
|
347
|
+
// Make grid overlay not intercept touch events
|
|
348
|
+
gridOverlayView.setClickable(false);
|
|
349
|
+
gridOverlayView.setFocusable(false);
|
|
350
|
+
previewContainer.addView(
|
|
351
|
+
gridOverlayView,
|
|
352
|
+
new FrameLayout.LayoutParams(
|
|
353
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
354
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
355
|
+
)
|
|
356
|
+
);
|
|
357
|
+
// Set grid mode after adding to container to ensure proper layout
|
|
358
|
+
gridOverlayView.post(() -> {
|
|
359
|
+
String currentGridMode = sessionConfig.getGridMode();
|
|
360
|
+
Log.d(TAG, "setupPreviewView: Setting grid mode to: " + currentGridMode);
|
|
361
|
+
gridOverlayView.setGridMode(currentGridMode);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Add a layout listener to update grid bounds when preview view changes size
|
|
365
|
+
previewView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
|
366
|
+
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
|
|
367
|
+
Log.d(TAG, "PreviewView layout changed, updating grid bounds");
|
|
368
|
+
updateGridOverlayBounds();
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
ViewGroup parent = (ViewGroup) webView.getParent();
|
|
373
|
+
if (parent != null) {
|
|
374
|
+
FrameLayout.LayoutParams layoutParams = calculatePreviewLayoutParams();
|
|
375
|
+
parent.addView(previewContainer, layoutParams);
|
|
376
|
+
if (sessionConfig.isToBack()) webView.bringToFront();
|
|
377
|
+
|
|
378
|
+
// Log the actual position after layout
|
|
379
|
+
previewContainer.post(() -> {
|
|
380
|
+
Log.d(TAG, "========================");
|
|
381
|
+
Log.d(TAG, "ACTUAL CAMERA VIEW POSITION (after layout):");
|
|
382
|
+
Log.d(
|
|
383
|
+
TAG,
|
|
384
|
+
"Container position - Left: " +
|
|
385
|
+
previewContainer.getLeft() +
|
|
386
|
+
", Top: " +
|
|
387
|
+
previewContainer.getTop() +
|
|
388
|
+
", Right: " +
|
|
389
|
+
previewContainer.getRight() +
|
|
390
|
+
", Bottom: " +
|
|
391
|
+
previewContainer.getBottom()
|
|
392
|
+
);
|
|
393
|
+
Log.d(
|
|
394
|
+
TAG,
|
|
395
|
+
"Container size - Width: " +
|
|
396
|
+
previewContainer.getWidth() +
|
|
397
|
+
", Height: " +
|
|
398
|
+
previewContainer.getHeight()
|
|
399
|
+
);
|
|
97
400
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
401
|
+
// Get parent info
|
|
402
|
+
ViewGroup containerParent = (ViewGroup) previewContainer.getParent();
|
|
403
|
+
if (containerParent != null) {
|
|
404
|
+
Log.d(
|
|
405
|
+
TAG,
|
|
406
|
+
"Parent class: " + containerParent.getClass().getSimpleName()
|
|
407
|
+
);
|
|
408
|
+
Log.d(
|
|
409
|
+
TAG,
|
|
410
|
+
"Parent size - Width: " +
|
|
411
|
+
containerParent.getWidth() +
|
|
412
|
+
", Height: " +
|
|
413
|
+
containerParent.getHeight()
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
Log.d(TAG, "========================");
|
|
417
|
+
});
|
|
102
418
|
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private FrameLayout.LayoutParams calculatePreviewLayoutParams() {
|
|
422
|
+
// sessionConfig already contains pixel-converted coordinates with webview offsets applied
|
|
423
|
+
int x = sessionConfig.getX();
|
|
424
|
+
int y = sessionConfig.getY();
|
|
425
|
+
int width = sessionConfig.getWidth();
|
|
426
|
+
int height = sessionConfig.getHeight();
|
|
427
|
+
String aspectRatio = sessionConfig.getAspectRatio();
|
|
428
|
+
|
|
429
|
+
Log.d(
|
|
430
|
+
TAG,
|
|
431
|
+
"calculatePreviewLayoutParams: Using sessionConfig values - x:" +
|
|
432
|
+
x +
|
|
433
|
+
" y:" +
|
|
434
|
+
y +
|
|
435
|
+
" width:" +
|
|
436
|
+
width +
|
|
437
|
+
" height:" +
|
|
438
|
+
height +
|
|
439
|
+
" aspectRatio:" +
|
|
440
|
+
aspectRatio
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
// Apply aspect ratio if specified and no explicit size was given
|
|
444
|
+
if (aspectRatio != null && !aspectRatio.isEmpty()) {
|
|
445
|
+
String[] ratios = aspectRatio.split(":");
|
|
446
|
+
if (ratios.length == 2) {
|
|
447
|
+
try {
|
|
448
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
449
|
+
float ratio =
|
|
450
|
+
Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
|
|
451
|
+
|
|
452
|
+
// Calculate optimal size while maintaining aspect ratio
|
|
453
|
+
int optimalWidth = width;
|
|
454
|
+
int optimalHeight = (int) (width / ratio);
|
|
455
|
+
|
|
456
|
+
if (optimalHeight > height) {
|
|
457
|
+
// Height constraint is tighter, fit by height
|
|
458
|
+
optimalHeight = height;
|
|
459
|
+
optimalWidth = (int) (height * ratio);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Store the old dimensions to check if we need to recenter
|
|
463
|
+
int oldWidth = width;
|
|
464
|
+
int oldHeight = height;
|
|
465
|
+
width = optimalWidth;
|
|
466
|
+
height = optimalHeight;
|
|
467
|
+
|
|
468
|
+
// If we're centered and dimensions changed, recalculate position
|
|
469
|
+
if (sessionConfig.isCentered()) {
|
|
470
|
+
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
|
471
|
+
|
|
472
|
+
if (width != oldWidth) {
|
|
473
|
+
int screenWidth = metrics.widthPixels;
|
|
474
|
+
x = (screenWidth - width) / 2;
|
|
475
|
+
Log.d(
|
|
476
|
+
TAG,
|
|
477
|
+
"calculatePreviewLayoutParams: Recentered X after aspect ratio - " +
|
|
478
|
+
"oldWidth=" +
|
|
479
|
+
oldWidth +
|
|
480
|
+
", newWidth=" +
|
|
481
|
+
width +
|
|
482
|
+
", screenWidth=" +
|
|
483
|
+
screenWidth +
|
|
484
|
+
", newX=" +
|
|
485
|
+
x
|
|
486
|
+
);
|
|
487
|
+
}
|
|
103
488
|
|
|
104
|
-
|
|
105
|
-
|
|
489
|
+
if (height != oldHeight) {
|
|
490
|
+
int screenHeight = metrics.heightPixels;
|
|
491
|
+
// Always center based on full screen height
|
|
492
|
+
y = (screenHeight - height) / 2;
|
|
493
|
+
Log.d(
|
|
494
|
+
TAG,
|
|
495
|
+
"calculatePreviewLayoutParams: Recentered Y after aspect ratio - " +
|
|
496
|
+
"oldHeight=" +
|
|
497
|
+
oldHeight +
|
|
498
|
+
", newHeight=" +
|
|
499
|
+
height +
|
|
500
|
+
", screenHeight=" +
|
|
501
|
+
screenHeight +
|
|
502
|
+
", newY=" +
|
|
503
|
+
y
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
Log.d(
|
|
509
|
+
TAG,
|
|
510
|
+
"calculatePreviewLayoutParams: Applied aspect ratio " +
|
|
511
|
+
aspectRatio +
|
|
512
|
+
" - new size: " +
|
|
513
|
+
width +
|
|
514
|
+
"x" +
|
|
515
|
+
height
|
|
516
|
+
);
|
|
517
|
+
} catch (NumberFormatException e) {
|
|
518
|
+
Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
106
521
|
}
|
|
107
522
|
|
|
108
|
-
|
|
109
|
-
|
|
523
|
+
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
|
|
524
|
+
width,
|
|
525
|
+
height
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// The X and Y positions passed from CameraPreview already include webView insets
|
|
529
|
+
// when edge-to-edge is active, so we don't need to add them again here
|
|
530
|
+
layoutParams.leftMargin = x;
|
|
531
|
+
layoutParams.topMargin = y;
|
|
532
|
+
|
|
533
|
+
Log.d(
|
|
534
|
+
TAG,
|
|
535
|
+
"calculatePreviewLayoutParams: Position calculation - x:" +
|
|
536
|
+
x +
|
|
537
|
+
" (leftMargin=" +
|
|
538
|
+
layoutParams.leftMargin +
|
|
539
|
+
"), y:" +
|
|
540
|
+
y +
|
|
541
|
+
" (topMargin=" +
|
|
542
|
+
layoutParams.topMargin +
|
|
543
|
+
")"
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
Log.d(
|
|
547
|
+
TAG,
|
|
548
|
+
"calculatePreviewLayoutParams: Final layout - x:" +
|
|
549
|
+
x +
|
|
550
|
+
" y:" +
|
|
551
|
+
y +
|
|
552
|
+
" width:" +
|
|
553
|
+
width +
|
|
554
|
+
" height:" +
|
|
555
|
+
height
|
|
556
|
+
);
|
|
557
|
+
return layoutParams;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private void removePreviewView() {
|
|
561
|
+
if (previewContainer != null) {
|
|
562
|
+
ViewGroup parent = (ViewGroup) previewContainer.getParent();
|
|
563
|
+
if (parent != null) {
|
|
564
|
+
parent.removeView(previewContainer);
|
|
565
|
+
}
|
|
566
|
+
previewContainer = null;
|
|
567
|
+
}
|
|
568
|
+
if (previewView != null) {
|
|
569
|
+
previewView = null;
|
|
570
|
+
}
|
|
571
|
+
if (gridOverlayView != null) {
|
|
572
|
+
gridOverlayView = null;
|
|
110
573
|
}
|
|
574
|
+
if (focusIndicatorView != null) {
|
|
575
|
+
focusIndicatorView = null;
|
|
576
|
+
}
|
|
577
|
+
webView.setBackgroundColor(android.graphics.Color.WHITE);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
581
|
+
private void bindCameraUseCases() {
|
|
582
|
+
if (cameraProvider == null) return;
|
|
583
|
+
mainExecutor.execute(() -> {
|
|
584
|
+
try {
|
|
585
|
+
Log.d(
|
|
586
|
+
TAG,
|
|
587
|
+
"Building camera selector with deviceId: " +
|
|
588
|
+
sessionConfig.getDeviceId() +
|
|
589
|
+
" and position: " +
|
|
590
|
+
sessionConfig.getPosition()
|
|
591
|
+
);
|
|
592
|
+
currentCameraSelector = buildCameraSelector();
|
|
111
593
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
"IMG_" + new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new java.util.Date()) + ".jpg"
|
|
594
|
+
ResolutionSelector.Builder resolutionSelectorBuilder =
|
|
595
|
+
new ResolutionSelector.Builder()
|
|
596
|
+
.setResolutionStrategy(
|
|
597
|
+
ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
|
|
117
598
|
);
|
|
118
|
-
FileOutputStream fos = new FileOutputStream(photo);
|
|
119
|
-
fos.write(data);
|
|
120
|
-
fos.close();
|
|
121
|
-
|
|
122
|
-
// Notify the gallery of the new image
|
|
123
|
-
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
|
124
|
-
Uri contentUri = Uri.fromFile(photo);
|
|
125
|
-
mediaScanIntent.setData(contentUri);
|
|
126
|
-
context.sendBroadcast(mediaScanIntent);
|
|
127
|
-
} catch (IOException e) {
|
|
128
|
-
Log.e(TAG, "Error saving image to gallery", e);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
public void startSession(CameraSessionConfiguration config) {
|
|
133
|
-
this.sessionConfig = config;
|
|
134
|
-
cameraExecutor = Executors.newSingleThreadExecutor();
|
|
135
|
-
mainExecutor.execute(() -> {
|
|
136
|
-
lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
|
|
137
|
-
setupCamera();
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
599
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
600
|
+
if (sessionConfig.getAspectRatio() != null) {
|
|
601
|
+
int aspectRatio;
|
|
602
|
+
if ("16:9".equals(sessionConfig.getAspectRatio())) {
|
|
603
|
+
aspectRatio = AspectRatio.RATIO_16_9;
|
|
604
|
+
} else { // "4:3"
|
|
605
|
+
aspectRatio = AspectRatio.RATIO_4_3;
|
|
606
|
+
}
|
|
607
|
+
resolutionSelectorBuilder.setAspectRatioStrategy(
|
|
608
|
+
new AspectRatioStrategy(
|
|
609
|
+
aspectRatio,
|
|
610
|
+
AspectRatioStrategy.FALLBACK_RULE_AUTO
|
|
611
|
+
)
|
|
612
|
+
);
|
|
613
|
+
}
|
|
154
614
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
615
|
+
ResolutionSelector resolutionSelector =
|
|
616
|
+
resolutionSelectorBuilder.build();
|
|
617
|
+
|
|
618
|
+
Preview preview = new Preview.Builder()
|
|
619
|
+
.setResolutionSelector(resolutionSelector)
|
|
620
|
+
.build();
|
|
621
|
+
imageCapture = new ImageCapture.Builder()
|
|
622
|
+
.setResolutionSelector(resolutionSelector)
|
|
623
|
+
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
624
|
+
.setFlashMode(currentFlashMode)
|
|
625
|
+
.build();
|
|
626
|
+
sampleImageCapture = imageCapture;
|
|
627
|
+
preview.setSurfaceProvider(previewView.getSurfaceProvider());
|
|
628
|
+
// Unbind any existing use cases and bind new ones
|
|
629
|
+
cameraProvider.unbindAll();
|
|
630
|
+
camera = cameraProvider.bindToLifecycle(
|
|
631
|
+
this,
|
|
632
|
+
currentCameraSelector,
|
|
633
|
+
preview,
|
|
634
|
+
imageCapture
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
// Log details about the active camera
|
|
638
|
+
Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
|
|
639
|
+
CameraInfo cameraInfo = camera.getCameraInfo();
|
|
640
|
+
Log.d(
|
|
641
|
+
TAG,
|
|
642
|
+
"Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId()
|
|
643
|
+
);
|
|
169
644
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
645
|
+
// Log zoom state
|
|
646
|
+
ZoomState zoomState = cameraInfo.getZoomState().getValue();
|
|
647
|
+
if (zoomState != null) {
|
|
648
|
+
Log.d(
|
|
649
|
+
TAG,
|
|
650
|
+
"Active Zoom State: " +
|
|
651
|
+
"min=" +
|
|
652
|
+
zoomState.getMinZoomRatio() +
|
|
653
|
+
", " +
|
|
654
|
+
"max=" +
|
|
655
|
+
zoomState.getMaxZoomRatio() +
|
|
656
|
+
", " +
|
|
657
|
+
"current=" +
|
|
658
|
+
zoomState.getZoomRatio()
|
|
659
|
+
);
|
|
173
660
|
}
|
|
174
|
-
|
|
175
|
-
|
|
661
|
+
|
|
662
|
+
// Log physical cameras of the active camera
|
|
663
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
664
|
+
Set<CameraInfo> physicalCameras = cameraInfo.getPhysicalCameraInfos();
|
|
665
|
+
Log.d(
|
|
666
|
+
TAG,
|
|
667
|
+
"Active camera has " + physicalCameras.size() + " physical cameras."
|
|
668
|
+
);
|
|
669
|
+
for (CameraInfo physical : physicalCameras) {
|
|
670
|
+
Log.d(
|
|
671
|
+
TAG,
|
|
672
|
+
" - Physical camera ID: " +
|
|
673
|
+
Camera2CameraInfo.from(physical).getCameraId()
|
|
674
|
+
);
|
|
675
|
+
}
|
|
176
676
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
677
|
+
|
|
678
|
+
// Log resolution info
|
|
679
|
+
ResolutionInfo previewResolution = preview.getResolutionInfo();
|
|
680
|
+
if (previewResolution != null) {
|
|
681
|
+
currentPreviewResolution = previewResolution.getResolution();
|
|
682
|
+
Log.d(TAG, "Preview resolution: " + currentPreviewResolution);
|
|
683
|
+
}
|
|
684
|
+
ResolutionInfo imageCaptureResolution =
|
|
685
|
+
imageCapture.getResolutionInfo();
|
|
686
|
+
if (imageCaptureResolution != null) {
|
|
687
|
+
Log.d(
|
|
688
|
+
TAG,
|
|
689
|
+
"Image capture resolution: " +
|
|
690
|
+
imageCaptureResolution.getResolution()
|
|
691
|
+
);
|
|
183
692
|
}
|
|
184
|
-
}
|
|
185
693
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
694
|
+
// Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
|
|
695
|
+
float initialZoom = sessionConfig.getTargetZoom() != 1.0f
|
|
696
|
+
? sessionConfig.getTargetZoom()
|
|
697
|
+
: sessionConfig.getZoomFactor();
|
|
698
|
+
if (initialZoom != 1.0f) {
|
|
699
|
+
Log.d(TAG, "Applying initial zoom of " + initialZoom);
|
|
700
|
+
|
|
701
|
+
// Validate zoom is within bounds
|
|
702
|
+
if (zoomState != null) {
|
|
703
|
+
float minZoom = zoomState.getMinZoomRatio();
|
|
704
|
+
float maxZoom = zoomState.getMaxZoomRatio();
|
|
705
|
+
|
|
706
|
+
if (initialZoom < minZoom || initialZoom > maxZoom) {
|
|
707
|
+
if (listener != null) {
|
|
708
|
+
listener.onCameraStartError(
|
|
709
|
+
"Initial zoom level " +
|
|
710
|
+
initialZoom +
|
|
711
|
+
" is not available. " +
|
|
712
|
+
"Valid range is " +
|
|
713
|
+
minZoom +
|
|
714
|
+
" to " +
|
|
715
|
+
maxZoom
|
|
716
|
+
);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
191
719
|
}
|
|
192
|
-
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
setZoomInternal(initialZoom);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
isRunning = true;
|
|
726
|
+
Log.d(TAG, "bindCameraUseCases: Camera bound successfully");
|
|
727
|
+
if (listener != null) {
|
|
728
|
+
// Post the callback to ensure layout is complete
|
|
729
|
+
previewContainer.post(() -> {
|
|
730
|
+
// Return actual preview container dimensions instead of requested dimensions
|
|
731
|
+
// Get the actual camera dimensions and position
|
|
732
|
+
int actualWidth = getPreviewWidth();
|
|
733
|
+
int actualHeight = getPreviewHeight();
|
|
734
|
+
int actualX = getPreviewX();
|
|
735
|
+
int actualY = getPreviewY();
|
|
736
|
+
|
|
737
|
+
Log.d(
|
|
738
|
+
TAG,
|
|
739
|
+
"onCameraStarted callback - actualX=" +
|
|
740
|
+
actualX +
|
|
741
|
+
", actualY=" +
|
|
742
|
+
actualY +
|
|
743
|
+
", actualWidth=" +
|
|
744
|
+
actualWidth +
|
|
745
|
+
", actualHeight=" +
|
|
746
|
+
actualHeight
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
// Update grid overlay bounds after camera is started
|
|
750
|
+
updateGridOverlayBounds();
|
|
751
|
+
|
|
752
|
+
listener.onCameraStarted(
|
|
753
|
+
actualWidth,
|
|
754
|
+
actualHeight,
|
|
755
|
+
actualX,
|
|
756
|
+
actualY
|
|
757
|
+
);
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
} catch (Exception e) {
|
|
761
|
+
if (listener != null) listener.onCameraStartError(
|
|
762
|
+
"Error binding camera: " + e.getMessage()
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
769
|
+
private CameraSelector buildCameraSelector() {
|
|
770
|
+
CameraSelector.Builder builder = new CameraSelector.Builder();
|
|
771
|
+
final String deviceId = sessionConfig.getDeviceId();
|
|
772
|
+
|
|
773
|
+
if (deviceId != null && !deviceId.isEmpty()) {
|
|
774
|
+
builder.addCameraFilter(cameraInfos -> {
|
|
775
|
+
for (CameraInfo cameraInfo : cameraInfos) {
|
|
776
|
+
if (
|
|
777
|
+
deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())
|
|
778
|
+
) {
|
|
779
|
+
return Collections.singletonList(cameraInfo);
|
|
780
|
+
}
|
|
193
781
|
}
|
|
194
|
-
|
|
782
|
+
return Collections.emptyList();
|
|
783
|
+
});
|
|
784
|
+
} else {
|
|
785
|
+
String position = sessionConfig.getPosition();
|
|
786
|
+
int requiredFacing = "front".equals(position)
|
|
787
|
+
? CameraSelector.LENS_FACING_FRONT
|
|
788
|
+
: CameraSelector.LENS_FACING_BACK;
|
|
789
|
+
builder.requireLensFacing(requiredFacing);
|
|
790
|
+
}
|
|
791
|
+
return builder.build();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private static String getCameraId(
|
|
795
|
+
androidx.camera.core.CameraInfo cameraInfo
|
|
796
|
+
) {
|
|
797
|
+
try {
|
|
798
|
+
// Generate a stable ID based on camera characteristics
|
|
799
|
+
boolean isBack = isBackCamera(cameraInfo);
|
|
800
|
+
float minZoom = Objects.requireNonNull(
|
|
801
|
+
cameraInfo.getZoomState().getValue()
|
|
802
|
+
).getMinZoomRatio();
|
|
803
|
+
float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
|
|
804
|
+
|
|
805
|
+
// Create a unique ID based on camera properties
|
|
806
|
+
String position = isBack ? "back" : "front";
|
|
807
|
+
return position + "_" + minZoom + "_" + maxZoom;
|
|
808
|
+
} catch (Exception e) {
|
|
809
|
+
return "unknown_camera";
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
private static boolean isBackCamera(
|
|
814
|
+
androidx.camera.core.CameraInfo cameraInfo
|
|
815
|
+
) {
|
|
816
|
+
try {
|
|
817
|
+
// Check if this camera matches the back camera selector
|
|
818
|
+
CameraSelector backSelector = new CameraSelector.Builder()
|
|
819
|
+
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
|
820
|
+
.build();
|
|
821
|
+
|
|
822
|
+
// Try to filter cameras with back selector - if this camera is included, it's a back camera
|
|
823
|
+
List<androidx.camera.core.CameraInfo> backCameras = backSelector.filter(
|
|
824
|
+
Collections.singletonList(cameraInfo)
|
|
825
|
+
);
|
|
826
|
+
return !backCameras.isEmpty();
|
|
827
|
+
} catch (Exception e) {
|
|
828
|
+
Log.w(TAG, "Error determining camera direction, assuming back camera", e);
|
|
829
|
+
return true; // Default to back camera
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
public void capturePhoto(
|
|
834
|
+
int quality,
|
|
835
|
+
final boolean saveToGallery,
|
|
836
|
+
Integer width,
|
|
837
|
+
Integer height,
|
|
838
|
+
String aspectRatio,
|
|
839
|
+
Location location
|
|
840
|
+
) {
|
|
841
|
+
Log.d(TAG, "capturePhoto: Starting photo capture with quality: " + quality + ", width: " + width + ", height: " + height + ", aspectRatio: " + aspectRatio);
|
|
842
|
+
|
|
843
|
+
// Check for conflicting parameters
|
|
844
|
+
if (aspectRatio != null && (width != null || height != null)) {
|
|
845
|
+
if (listener != null) {
|
|
846
|
+
listener.onPictureTakenError("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.");
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
195
849
|
}
|
|
196
850
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
currentCameraSelector = buildCameraSelector();
|
|
204
|
-
ResolutionSelector resolutionSelector = new ResolutionSelector.Builder()
|
|
205
|
-
.setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY)
|
|
206
|
-
.build();
|
|
207
|
-
Preview preview = new Preview.Builder().setResolutionSelector(resolutionSelector).build();
|
|
208
|
-
imageCapture = new ImageCapture.Builder().setResolutionSelector(resolutionSelector).setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).setFlashMode(currentFlashMode).build();
|
|
209
|
-
sampleImageCapture = imageCapture;
|
|
210
|
-
preview.setSurfaceProvider(previewView.getSurfaceProvider());
|
|
211
|
-
// Unbind any existing use cases and bind new ones
|
|
212
|
-
cameraProvider.unbindAll();
|
|
213
|
-
camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture);
|
|
214
|
-
|
|
215
|
-
// Log details about the active camera
|
|
216
|
-
Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
|
|
217
|
-
CameraInfo cameraInfo = camera.getCameraInfo();
|
|
218
|
-
Log.d(TAG, "Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId());
|
|
219
|
-
|
|
220
|
-
// Log zoom state
|
|
221
|
-
ZoomState zoomState = cameraInfo.getZoomState().getValue();
|
|
222
|
-
if (zoomState != null) {
|
|
223
|
-
Log.d(TAG, "Active Zoom State: " +
|
|
224
|
-
"min=" + zoomState.getMinZoomRatio() + ", " +
|
|
225
|
-
"max=" + zoomState.getMaxZoomRatio() + ", " +
|
|
226
|
-
"current=" + zoomState.getZoomRatio());
|
|
227
|
-
}
|
|
851
|
+
if (imageCapture == null) {
|
|
852
|
+
if (listener != null) {
|
|
853
|
+
listener.onPictureTakenError("Camera not ready");
|
|
854
|
+
}
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
228
857
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
858
|
+
File tempFile = new File(context.getCacheDir(), "temp_image.jpg");
|
|
859
|
+
ImageCapture.OutputFileOptions outputFileOptions =
|
|
860
|
+
new ImageCapture.OutputFileOptions.Builder(tempFile).build();
|
|
861
|
+
|
|
862
|
+
imageCapture.takePicture(
|
|
863
|
+
outputFileOptions,
|
|
864
|
+
cameraExecutor,
|
|
865
|
+
new ImageCapture.OnImageSavedCallback() {
|
|
866
|
+
@Override
|
|
867
|
+
public void onError(@NonNull ImageCaptureException exception) {
|
|
868
|
+
Log.e(TAG, "capturePhoto: Photo capture failed", exception);
|
|
869
|
+
if (listener != null) {
|
|
870
|
+
listener.onPictureTakenError(
|
|
871
|
+
"Photo capture failed: " + exception.getMessage()
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
237
875
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
876
|
+
@Override
|
|
877
|
+
public void onImageSaved(
|
|
878
|
+
@NonNull ImageCapture.OutputFileResults output
|
|
879
|
+
) {
|
|
880
|
+
try {
|
|
881
|
+
// Read file using FileInputStream for compatibility
|
|
882
|
+
byte[] bytes = new byte[(int) tempFile.length()];
|
|
883
|
+
java.io.FileInputStream fis = new java.io.FileInputStream(tempFile);
|
|
884
|
+
fis.read(bytes);
|
|
885
|
+
fis.close();
|
|
886
|
+
|
|
887
|
+
ExifInterface exifInterface = new ExifInterface(
|
|
888
|
+
tempFile.getAbsolutePath()
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
if (location != null) {
|
|
892
|
+
exifInterface.setGpsInfo(location);
|
|
893
|
+
}
|
|
247
894
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
895
|
+
JSONObject exifData = getExifData(exifInterface);
|
|
896
|
+
|
|
897
|
+
// Use the stored aspectRatio if none is provided and no width/height is specified
|
|
898
|
+
String captureAspectRatio = aspectRatio;
|
|
899
|
+
if (width == null && height == null && aspectRatio == null && sessionConfig != null) {
|
|
900
|
+
captureAspectRatio = sessionConfig.getAspectRatio();
|
|
901
|
+
// Default to "4:3" if no aspect ratio was set at all
|
|
902
|
+
if (captureAspectRatio == null) {
|
|
903
|
+
captureAspectRatio = "4:3";
|
|
904
|
+
}
|
|
905
|
+
Log.d(TAG, "capturePhoto: Using stored aspectRatio: " + captureAspectRatio);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Handle aspect ratio if no width/height specified
|
|
909
|
+
if (width == null && height == null && captureAspectRatio != null && !captureAspectRatio.isEmpty()) {
|
|
910
|
+
// Get the original image dimensions
|
|
911
|
+
Bitmap originalBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
|
912
|
+
int originalWidth = originalBitmap.getWidth();
|
|
913
|
+
int originalHeight = originalBitmap.getHeight();
|
|
914
|
+
|
|
915
|
+
// Parse aspect ratio
|
|
916
|
+
String[] ratios = captureAspectRatio.split(":");
|
|
917
|
+
if (ratios.length == 2) {
|
|
918
|
+
try {
|
|
919
|
+
float widthRatio = Float.parseFloat(ratios[0]);
|
|
920
|
+
float heightRatio = Float.parseFloat(ratios[1]);
|
|
921
|
+
|
|
922
|
+
// For capture in portrait orientation, swap the aspect ratio (16:9 becomes 9:16)
|
|
923
|
+
boolean isPortrait = originalHeight > originalWidth;
|
|
924
|
+
float targetAspectRatio = isPortrait ? heightRatio / widthRatio : widthRatio / heightRatio;
|
|
925
|
+
float originalAspectRatio = (float) originalWidth / originalHeight;
|
|
926
|
+
|
|
927
|
+
int targetWidth, targetHeight;
|
|
928
|
+
|
|
929
|
+
if (originalAspectRatio > targetAspectRatio) {
|
|
930
|
+
// Original is wider than target - fit by height
|
|
931
|
+
targetHeight = originalHeight;
|
|
932
|
+
targetWidth = (int) (targetHeight * targetAspectRatio);
|
|
933
|
+
} else {
|
|
934
|
+
// Original is taller than target - fit by width
|
|
935
|
+
targetWidth = originalWidth;
|
|
936
|
+
targetHeight = (int) (targetWidth / targetAspectRatio);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Center crop the image
|
|
940
|
+
int xOffset = (originalWidth - targetWidth) / 2;
|
|
941
|
+
int yOffset = (originalHeight - targetHeight) / 2;
|
|
942
|
+
|
|
943
|
+
Bitmap croppedBitmap = Bitmap.createBitmap(
|
|
944
|
+
originalBitmap,
|
|
945
|
+
xOffset,
|
|
946
|
+
yOffset,
|
|
947
|
+
targetWidth,
|
|
948
|
+
targetHeight
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
952
|
+
croppedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
|
|
953
|
+
bytes = stream.toByteArray();
|
|
954
|
+
|
|
955
|
+
// Write EXIF data back to cropped image
|
|
956
|
+
bytes = writeExifToImageBytes(bytes, exifInterface);
|
|
957
|
+
|
|
958
|
+
originalBitmap.recycle();
|
|
959
|
+
croppedBitmap.recycle();
|
|
960
|
+
} catch (NumberFormatException e) {
|
|
961
|
+
Log.e(TAG, "Invalid aspect ratio format: " + captureAspectRatio, e);
|
|
253
962
|
}
|
|
963
|
+
}
|
|
964
|
+
} else if (width != null && height != null) {
|
|
965
|
+
Bitmap bitmap = BitmapFactory.decodeByteArray(
|
|
966
|
+
bytes,
|
|
967
|
+
0,
|
|
968
|
+
bytes.length
|
|
969
|
+
);
|
|
970
|
+
Bitmap resizedBitmap = resizeBitmap(bitmap, width, height);
|
|
971
|
+
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
972
|
+
resizedBitmap.compress(
|
|
973
|
+
Bitmap.CompressFormat.JPEG,
|
|
974
|
+
quality,
|
|
975
|
+
stream
|
|
976
|
+
);
|
|
977
|
+
bytes = stream.toByteArray();
|
|
978
|
+
|
|
979
|
+
// Write EXIF data back to resized image
|
|
980
|
+
bytes = writeExifToImageBytes(bytes, exifInterface);
|
|
981
|
+
} else {
|
|
982
|
+
// For non-resized images, ensure EXIF is saved
|
|
983
|
+
exifInterface.saveAttributes();
|
|
984
|
+
bytes = new byte[(int) tempFile.length()];
|
|
985
|
+
java.io.FileInputStream fis2 = new java.io.FileInputStream(
|
|
986
|
+
tempFile
|
|
987
|
+
);
|
|
988
|
+
fis2.read(bytes);
|
|
989
|
+
fis2.close();
|
|
990
|
+
}
|
|
254
991
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (listener != null) listener.onCameraStarted();
|
|
258
|
-
} catch (Exception e) {
|
|
259
|
-
if (listener != null) listener.onCameraStartError("Error binding camera: " + e.getMessage());
|
|
992
|
+
if (saveToGallery) {
|
|
993
|
+
saveImageToGallery(bytes);
|
|
260
994
|
}
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
995
|
|
|
264
|
-
|
|
265
|
-
private CameraSelector buildCameraSelector() {
|
|
266
|
-
CameraSelector.Builder builder = new CameraSelector.Builder();
|
|
267
|
-
final String deviceId = sessionConfig.getDeviceId();
|
|
996
|
+
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
268
997
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
998
|
+
tempFile.delete();
|
|
999
|
+
|
|
1000
|
+
if (listener != null) {
|
|
1001
|
+
listener.onPictureTaken(base64, exifData);
|
|
1002
|
+
}
|
|
1003
|
+
} catch (Exception e) {
|
|
1004
|
+
Log.e(TAG, "capturePhoto: Error processing image", e);
|
|
1005
|
+
if (listener != null) {
|
|
1006
|
+
listener.onPictureTakenError(
|
|
1007
|
+
"Error processing image: " + e.getMessage()
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private Bitmap resizeBitmap(Bitmap bitmap, int width, int height) {
|
|
1017
|
+
return Bitmap.createScaledBitmap(bitmap, width, height, true);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
private JSONObject getExifData(ExifInterface exifInterface) {
|
|
1021
|
+
JSONObject exifData = new JSONObject();
|
|
1022
|
+
try {
|
|
1023
|
+
// Add all available exif tags to a JSON object
|
|
1024
|
+
for (String[] tag : EXIF_TAGS) {
|
|
1025
|
+
String value = exifInterface.getAttribute(tag[0]);
|
|
1026
|
+
if (value != null) {
|
|
1027
|
+
exifData.put(tag[1], value);
|
|
282
1028
|
}
|
|
283
|
-
|
|
1029
|
+
}
|
|
1030
|
+
} catch (Exception e) {
|
|
1031
|
+
Log.e(TAG, "getExifData: Error reading exif data", e);
|
|
284
1032
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
1033
|
+
return exifData;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private static final String[][] EXIF_TAGS = new String[][] {
|
|
1037
|
+
{ ExifInterface.TAG_APERTURE_VALUE, "ApertureValue" },
|
|
1038
|
+
{ ExifInterface.TAG_ARTIST, "Artist" },
|
|
1039
|
+
{ ExifInterface.TAG_BITS_PER_SAMPLE, "BitsPerSample" },
|
|
1040
|
+
{ ExifInterface.TAG_BRIGHTNESS_VALUE, "BrightnessValue" },
|
|
1041
|
+
{ ExifInterface.TAG_CFA_PATTERN, "CFAPattern" },
|
|
1042
|
+
{ ExifInterface.TAG_COLOR_SPACE, "ColorSpace" },
|
|
1043
|
+
{ ExifInterface.TAG_COMPONENTS_CONFIGURATION, "ComponentsConfiguration" },
|
|
1044
|
+
{ ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, "CompressedBitsPerPixel" },
|
|
1045
|
+
{ ExifInterface.TAG_COMPRESSION, "Compression" },
|
|
1046
|
+
{ ExifInterface.TAG_CONTRAST, "Contrast" },
|
|
1047
|
+
{ ExifInterface.TAG_COPYRIGHT, "Copyright" },
|
|
1048
|
+
{ ExifInterface.TAG_CUSTOM_RENDERED, "CustomRendered" },
|
|
1049
|
+
{ ExifInterface.TAG_DATETIME, "DateTime" },
|
|
1050
|
+
{ ExifInterface.TAG_DATETIME_DIGITIZED, "DateTimeDigitized" },
|
|
1051
|
+
{ ExifInterface.TAG_DATETIME_ORIGINAL, "DateTimeOriginal" },
|
|
1052
|
+
{
|
|
1053
|
+
ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
|
|
1054
|
+
"DeviceSettingDescription",
|
|
1055
|
+
},
|
|
1056
|
+
{ ExifInterface.TAG_DIGITAL_ZOOM_RATIO, "DigitalZoomRatio" },
|
|
1057
|
+
{ ExifInterface.TAG_DNG_VERSION, "DNGVersion" },
|
|
1058
|
+
{ ExifInterface.TAG_EXIF_VERSION, "ExifVersion" },
|
|
1059
|
+
{ ExifInterface.TAG_EXPOSURE_BIAS_VALUE, "ExposureBiasValue" },
|
|
1060
|
+
{ ExifInterface.TAG_EXPOSURE_INDEX, "ExposureIndex" },
|
|
1061
|
+
{ ExifInterface.TAG_EXPOSURE_MODE, "ExposureMode" },
|
|
1062
|
+
{ ExifInterface.TAG_EXPOSURE_PROGRAM, "ExposureProgram" },
|
|
1063
|
+
{ ExifInterface.TAG_EXPOSURE_TIME, "ExposureTime" },
|
|
1064
|
+
{ ExifInterface.TAG_FILE_SOURCE, "FileSource" },
|
|
1065
|
+
{ ExifInterface.TAG_FLASH, "Flash" },
|
|
1066
|
+
{ ExifInterface.TAG_FLASHPIX_VERSION, "FlashpixVersion" },
|
|
1067
|
+
{ ExifInterface.TAG_FLASH_ENERGY, "FlashEnergy" },
|
|
1068
|
+
{ ExifInterface.TAG_FOCAL_LENGTH, "FocalLength" },
|
|
1069
|
+
{ ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, "FocalLengthIn35mmFilm" },
|
|
1070
|
+
{
|
|
1071
|
+
ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
|
|
1072
|
+
"FocalPlaneResolutionUnit",
|
|
1073
|
+
},
|
|
1074
|
+
{ ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, "FocalPlaneXResolution" },
|
|
1075
|
+
{ ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, "FocalPlaneYResolution" },
|
|
1076
|
+
{ ExifInterface.TAG_F_NUMBER, "FNumber" },
|
|
1077
|
+
{ ExifInterface.TAG_GAIN_CONTROL, "GainControl" },
|
|
1078
|
+
{ ExifInterface.TAG_GPS_ALTITUDE, "GPSAltitude" },
|
|
1079
|
+
{ ExifInterface.TAG_GPS_ALTITUDE_REF, "GPSAltitudeRef" },
|
|
1080
|
+
{ ExifInterface.TAG_GPS_AREA_INFORMATION, "GPSAreaInformation" },
|
|
1081
|
+
{ ExifInterface.TAG_GPS_DATESTAMP, "GPSDateStamp" },
|
|
1082
|
+
{ ExifInterface.TAG_GPS_DEST_BEARING, "GPSDestBearing" },
|
|
1083
|
+
{ ExifInterface.TAG_GPS_DEST_BEARING_REF, "GPSDestBearingRef" },
|
|
1084
|
+
{ ExifInterface.TAG_GPS_DEST_DISTANCE, "GPSDestDistance" },
|
|
1085
|
+
{ ExifInterface.TAG_GPS_DEST_DISTANCE_REF, "GPSDestDistanceRef" },
|
|
1086
|
+
{ ExifInterface.TAG_GPS_DEST_LATITUDE, "GPSDestLatitude" },
|
|
1087
|
+
{ ExifInterface.TAG_GPS_DEST_LATITUDE_REF, "GPSDestLatitudeRef" },
|
|
1088
|
+
{ ExifInterface.TAG_GPS_DEST_LONGITUDE, "GPSDestLongitude" },
|
|
1089
|
+
{ ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, "GPSDestLongitudeRef" },
|
|
1090
|
+
{ ExifInterface.TAG_GPS_DIFFERENTIAL, "GPSDifferential" },
|
|
1091
|
+
{ ExifInterface.TAG_GPS_DOP, "GPSDOP" },
|
|
1092
|
+
{ ExifInterface.TAG_GPS_IMG_DIRECTION, "GPSImgDirection" },
|
|
1093
|
+
{ ExifInterface.TAG_GPS_IMG_DIRECTION_REF, "GPSImgDirectionRef" },
|
|
1094
|
+
{ ExifInterface.TAG_GPS_LATITUDE, "GPSLatitude" },
|
|
1095
|
+
{ ExifInterface.TAG_GPS_LATITUDE_REF, "GPSLatitudeRef" },
|
|
1096
|
+
{ ExifInterface.TAG_GPS_LONGITUDE, "GPSLongitude" },
|
|
1097
|
+
{ ExifInterface.TAG_GPS_LONGITUDE_REF, "GPSLongitudeRef" },
|
|
1098
|
+
{ ExifInterface.TAG_GPS_MAP_DATUM, "GPSMapDatum" },
|
|
1099
|
+
{ ExifInterface.TAG_GPS_MEASURE_MODE, "GPSMeasureMode" },
|
|
1100
|
+
{ ExifInterface.TAG_GPS_PROCESSING_METHOD, "GPSProcessingMethod" },
|
|
1101
|
+
{ ExifInterface.TAG_GPS_SATELLITES, "GPSSatellites" },
|
|
1102
|
+
{ ExifInterface.TAG_GPS_SPEED, "GPSSpeed" },
|
|
1103
|
+
{ ExifInterface.TAG_GPS_SPEED_REF, "GPSSpeedRef" },
|
|
1104
|
+
{ ExifInterface.TAG_GPS_STATUS, "GPSStatus" },
|
|
1105
|
+
{ ExifInterface.TAG_GPS_TIMESTAMP, "GPSTimeStamp" },
|
|
1106
|
+
{ ExifInterface.TAG_GPS_TRACK, "GPSTrack" },
|
|
1107
|
+
{ ExifInterface.TAG_GPS_TRACK_REF, "GPSTrackRef" },
|
|
1108
|
+
{ ExifInterface.TAG_GPS_VERSION_ID, "GPSVersionID" },
|
|
1109
|
+
{ ExifInterface.TAG_IMAGE_DESCRIPTION, "ImageDescription" },
|
|
1110
|
+
{ ExifInterface.TAG_IMAGE_LENGTH, "ImageLength" },
|
|
1111
|
+
{ ExifInterface.TAG_IMAGE_UNIQUE_ID, "ImageUniqueID" },
|
|
1112
|
+
{ ExifInterface.TAG_IMAGE_WIDTH, "ImageWidth" },
|
|
1113
|
+
{ ExifInterface.TAG_INTEROPERABILITY_INDEX, "InteroperabilityIndex" },
|
|
1114
|
+
{ ExifInterface.TAG_ISO_SPEED, "ISOSpeed" },
|
|
1115
|
+
{ ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, "ISOSpeedLatitudeyyy" },
|
|
1116
|
+
{ ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, "ISOSpeedLatitudezzz" },
|
|
1117
|
+
{ ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, "JPEGInterchangeFormat" },
|
|
1118
|
+
{
|
|
1119
|
+
ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
|
|
1120
|
+
"JPEGInterchangeFormatLength",
|
|
1121
|
+
},
|
|
1122
|
+
{ ExifInterface.TAG_LIGHT_SOURCE, "LightSource" },
|
|
1123
|
+
{ ExifInterface.TAG_MAKE, "Make" },
|
|
1124
|
+
{ ExifInterface.TAG_MAKER_NOTE, "MakerNote" },
|
|
1125
|
+
{ ExifInterface.TAG_MAX_APERTURE_VALUE, "MaxApertureValue" },
|
|
1126
|
+
{ ExifInterface.TAG_METERING_MODE, "MeteringMode" },
|
|
1127
|
+
{ ExifInterface.TAG_MODEL, "Model" },
|
|
1128
|
+
{ ExifInterface.TAG_NEW_SUBFILE_TYPE, "NewSubfileType" },
|
|
1129
|
+
{ ExifInterface.TAG_OECF, "OECF" },
|
|
1130
|
+
{ ExifInterface.TAG_OFFSET_TIME, "OffsetTime" },
|
|
1131
|
+
{ ExifInterface.TAG_OFFSET_TIME_DIGITIZED, "OffsetTimeDigitized" },
|
|
1132
|
+
{ ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "OffsetTimeOriginal" },
|
|
1133
|
+
{ ExifInterface.TAG_ORF_ASPECT_FRAME, "ORFAspectFrame" },
|
|
1134
|
+
{ ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH, "ORFPreviewImageLength" },
|
|
1135
|
+
{ ExifInterface.TAG_ORF_PREVIEW_IMAGE_START, "ORFPreviewImageStart" },
|
|
1136
|
+
{ ExifInterface.TAG_ORF_THUMBNAIL_IMAGE, "ORFThumbnailImage" },
|
|
1137
|
+
{ ExifInterface.TAG_ORIENTATION, "Orientation" },
|
|
1138
|
+
{
|
|
1139
|
+
ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
|
|
1140
|
+
"PhotometricInterpretation",
|
|
1141
|
+
},
|
|
1142
|
+
{ ExifInterface.TAG_PIXEL_X_DIMENSION, "PixelXDimension" },
|
|
1143
|
+
{ ExifInterface.TAG_PIXEL_Y_DIMENSION, "PixelYDimension" },
|
|
1144
|
+
{ ExifInterface.TAG_PLANAR_CONFIGURATION, "PlanarConfiguration" },
|
|
1145
|
+
{ ExifInterface.TAG_PRIMARY_CHROMATICITIES, "PrimaryChromaticities" },
|
|
1146
|
+
{
|
|
1147
|
+
ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX,
|
|
1148
|
+
"RecommendedExposureIndex",
|
|
1149
|
+
},
|
|
1150
|
+
{ ExifInterface.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite" },
|
|
1151
|
+
{ ExifInterface.TAG_RELATED_SOUND_FILE, "RelatedSoundFile" },
|
|
1152
|
+
{ ExifInterface.TAG_RESOLUTION_UNIT, "ResolutionUnit" },
|
|
1153
|
+
{ ExifInterface.TAG_ROWS_PER_STRIP, "RowsPerStrip" },
|
|
1154
|
+
{ ExifInterface.TAG_RW2_ISO, "RW2ISO" },
|
|
1155
|
+
{ ExifInterface.TAG_RW2_JPG_FROM_RAW, "RW2JpgFromRaw" },
|
|
1156
|
+
{ ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER, "RW2SensorBottomBorder" },
|
|
1157
|
+
{ ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER, "RW2SensorLeftBorder" },
|
|
1158
|
+
{ ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, "RW2SensorRightBorder" },
|
|
1159
|
+
{ ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, "RW2SensorTopBorder" },
|
|
1160
|
+
{ ExifInterface.TAG_SAMPLES_PER_PIXEL, "SamplesPerPixel" },
|
|
1161
|
+
{ ExifInterface.TAG_SATURATION, "Saturation" },
|
|
1162
|
+
{ ExifInterface.TAG_SCENE_CAPTURE_TYPE, "SceneCaptureType" },
|
|
1163
|
+
{ ExifInterface.TAG_SCENE_TYPE, "SceneType" },
|
|
1164
|
+
{ ExifInterface.TAG_SENSING_METHOD, "SensingMethod" },
|
|
1165
|
+
{ ExifInterface.TAG_SENSITIVITY_TYPE, "SensitivityType" },
|
|
1166
|
+
{ ExifInterface.TAG_SHARPNESS, "Sharpness" },
|
|
1167
|
+
{ ExifInterface.TAG_SHUTTER_SPEED_VALUE, "ShutterSpeedValue" },
|
|
1168
|
+
{ ExifInterface.TAG_SOFTWARE, "Software" },
|
|
1169
|
+
{
|
|
1170
|
+
ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE,
|
|
1171
|
+
"SpatialFrequencyResponse",
|
|
1172
|
+
},
|
|
1173
|
+
{ ExifInterface.TAG_SPECTRAL_SENSITIVITY, "SpectralSensitivity" },
|
|
1174
|
+
{
|
|
1175
|
+
ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY,
|
|
1176
|
+
"StandardOutputSensitivity",
|
|
1177
|
+
},
|
|
1178
|
+
{ ExifInterface.TAG_STRIP_BYTE_COUNTS, "StripByteCounts" },
|
|
1179
|
+
{ ExifInterface.TAG_STRIP_OFFSETS, "StripOffsets" },
|
|
1180
|
+
{ ExifInterface.TAG_SUBFILE_TYPE, "SubfileType" },
|
|
1181
|
+
{ ExifInterface.TAG_SUBJECT_AREA, "SubjectArea" },
|
|
1182
|
+
{ ExifInterface.TAG_SUBJECT_DISTANCE, "SubjectDistance" },
|
|
1183
|
+
{ ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, "SubjectDistanceRange" },
|
|
1184
|
+
{ ExifInterface.TAG_SUBJECT_LOCATION, "SubjectLocation" },
|
|
1185
|
+
{ ExifInterface.TAG_SUBSEC_TIME, "SubSecTime" },
|
|
1186
|
+
{ ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, "SubSecTimeDigitized" },
|
|
1187
|
+
{ ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, "SubSecTimeOriginal" },
|
|
1188
|
+
{ ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH, "ThumbnailImageLength" },
|
|
1189
|
+
{ ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH, "ThumbnailImageWidth" },
|
|
1190
|
+
{ ExifInterface.TAG_TRANSFER_FUNCTION, "TransferFunction" },
|
|
1191
|
+
{ ExifInterface.TAG_USER_COMMENT, "UserComment" },
|
|
1192
|
+
{ ExifInterface.TAG_WHITE_BALANCE, "WhiteBalance" },
|
|
1193
|
+
{ ExifInterface.TAG_WHITE_POINT, "WhitePoint" },
|
|
1194
|
+
{ ExifInterface.TAG_X_RESOLUTION, "XResolution" },
|
|
1195
|
+
{ ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, "YCbCrCoefficients" },
|
|
1196
|
+
{ ExifInterface.TAG_Y_CB_CR_POSITIONING, "YCbCrPositioning" },
|
|
1197
|
+
{ ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, "YCbCrSubSampling" },
|
|
1198
|
+
{ ExifInterface.TAG_Y_RESOLUTION, "YResolution" },
|
|
1199
|
+
};
|
|
1200
|
+
|
|
1201
|
+
private byte[] writeExifToImageBytes(
|
|
1202
|
+
byte[] imageBytes,
|
|
1203
|
+
ExifInterface sourceExif
|
|
1204
|
+
) {
|
|
1205
|
+
try {
|
|
1206
|
+
// Create a temporary file to write the image with EXIF
|
|
1207
|
+
File tempExifFile = File.createTempFile(
|
|
1208
|
+
"temp_exif",
|
|
1209
|
+
".jpg",
|
|
1210
|
+
context.getCacheDir()
|
|
1211
|
+
);
|
|
1212
|
+
|
|
1213
|
+
// Write the image bytes to temp file
|
|
1214
|
+
java.io.FileOutputStream fos = new java.io.FileOutputStream(tempExifFile);
|
|
1215
|
+
fos.write(imageBytes);
|
|
1216
|
+
fos.close();
|
|
1217
|
+
|
|
1218
|
+
// Create new ExifInterface for the temp file and copy all EXIF data
|
|
1219
|
+
ExifInterface newExif = new ExifInterface(tempExifFile.getAbsolutePath());
|
|
1220
|
+
|
|
1221
|
+
// Copy all EXIF attributes from source to new
|
|
1222
|
+
for (String[] tag : EXIF_TAGS) {
|
|
1223
|
+
String value = sourceExif.getAttribute(tag[0]);
|
|
1224
|
+
if (value != null) {
|
|
1225
|
+
newExif.setAttribute(tag[0], value);
|
|
298
1226
|
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Save the EXIF data
|
|
1230
|
+
newExif.saveAttributes();
|
|
1231
|
+
|
|
1232
|
+
// Read the file back with EXIF embedded
|
|
1233
|
+
byte[] result = new byte[(int) tempExifFile.length()];
|
|
1234
|
+
java.io.FileInputStream fis = new java.io.FileInputStream(tempExifFile);
|
|
1235
|
+
fis.read(result);
|
|
1236
|
+
fis.close();
|
|
1237
|
+
|
|
1238
|
+
// Clean up temp file
|
|
1239
|
+
tempExifFile.delete();
|
|
1240
|
+
|
|
1241
|
+
return result;
|
|
1242
|
+
} catch (Exception e) {
|
|
1243
|
+
Log.e(TAG, "writeExifToImageBytes: Error writing EXIF data", e);
|
|
1244
|
+
return imageBytes; // Return original bytes if error
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
public void captureSample(int quality) {
|
|
1249
|
+
Log.d(
|
|
1250
|
+
TAG,
|
|
1251
|
+
"captureSample: Starting sample capture with quality: " + quality
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
if (sampleImageCapture == null) {
|
|
1255
|
+
if (listener != null) {
|
|
1256
|
+
listener.onSampleTakenError("Camera not ready");
|
|
1257
|
+
}
|
|
1258
|
+
return;
|
|
299
1259
|
}
|
|
300
1260
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
Log.w(TAG, "Error determining camera direction, assuming back camera", e);
|
|
313
|
-
return true; // Default to back camera
|
|
1261
|
+
sampleImageCapture.takePicture(
|
|
1262
|
+
cameraExecutor,
|
|
1263
|
+
new ImageCapture.OnImageCapturedCallback() {
|
|
1264
|
+
@Override
|
|
1265
|
+
public void onError(@NonNull ImageCaptureException exception) {
|
|
1266
|
+
Log.e(TAG, "captureSample: Sample capture failed", exception);
|
|
1267
|
+
if (listener != null) {
|
|
1268
|
+
listener.onSampleTakenError(
|
|
1269
|
+
"Sample capture failed: " + exception.getMessage()
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
314
1272
|
}
|
|
315
|
-
}
|
|
316
1273
|
|
|
317
|
-
|
|
318
|
-
|
|
1274
|
+
@Override
|
|
1275
|
+
public void onCaptureSuccess(@NonNull ImageProxy image) {
|
|
1276
|
+
try {
|
|
1277
|
+
// Convert ImageProxy to byte array
|
|
1278
|
+
byte[] bytes = imageProxyToByteArray(image);
|
|
1279
|
+
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
319
1280
|
|
|
320
|
-
if (imageCapture == null) {
|
|
321
1281
|
if (listener != null) {
|
|
322
|
-
|
|
1282
|
+
listener.onSampleTaken(base64);
|
|
323
1283
|
}
|
|
324
|
-
|
|
1284
|
+
} catch (Exception e) {
|
|
1285
|
+
Log.e(TAG, "captureSample: Error processing sample", e);
|
|
1286
|
+
if (listener != null) {
|
|
1287
|
+
listener.onSampleTakenError(
|
|
1288
|
+
"Error processing sample: " + e.getMessage()
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
} finally {
|
|
1292
|
+
image.close();
|
|
1293
|
+
}
|
|
325
1294
|
}
|
|
1295
|
+
}
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
private byte[] imageProxyToByteArray(ImageProxy image) {
|
|
1300
|
+
ImageProxy.PlaneProxy[] planes = image.getPlanes();
|
|
1301
|
+
ByteBuffer buffer = planes[0].getBuffer();
|
|
1302
|
+
byte[] bytes = new byte[buffer.remaining()];
|
|
1303
|
+
buffer.get(bytes);
|
|
1304
|
+
return bytes;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// not workin for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
|
|
1308
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
1309
|
+
public static List<
|
|
1310
|
+
com.ahm.capacitor.camera.preview.model.CameraDevice
|
|
1311
|
+
> getAvailableDevicesStatic(Context context) {
|
|
1312
|
+
Log.d(
|
|
1313
|
+
TAG,
|
|
1314
|
+
"getAvailableDevicesStatic: Starting CameraX device enumeration with getPhysicalCameraInfos."
|
|
1315
|
+
);
|
|
1316
|
+
List<com.ahm.capacitor.camera.preview.model.CameraDevice> devices =
|
|
1317
|
+
new ArrayList<>();
|
|
1318
|
+
try {
|
|
1319
|
+
ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
|
|
1320
|
+
ProcessCameraProvider.getInstance(context);
|
|
1321
|
+
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
|
|
1322
|
+
CameraManager cameraManager = (CameraManager) context.getSystemService(
|
|
1323
|
+
Context.CAMERA_SERVICE
|
|
1324
|
+
);
|
|
1325
|
+
|
|
1326
|
+
for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
|
|
1327
|
+
String logicalCameraId = Camera2CameraInfo.from(
|
|
1328
|
+
cameraInfo
|
|
1329
|
+
).getCameraId();
|
|
1330
|
+
String position = isBackCamera(cameraInfo) ? "rear" : "front";
|
|
1331
|
+
|
|
1332
|
+
// Add logical camera
|
|
1333
|
+
float minZoom = Objects.requireNonNull(
|
|
1334
|
+
cameraInfo.getZoomState().getValue()
|
|
1335
|
+
).getMinZoomRatio();
|
|
1336
|
+
float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
|
|
1337
|
+
List<LensInfo> logicalLenses = new ArrayList<>();
|
|
1338
|
+
logicalLenses.add(new LensInfo(4.25f, "wideAngle", 1.0f, maxZoom));
|
|
1339
|
+
devices.add(
|
|
1340
|
+
new com.ahm.capacitor.camera.preview.model.CameraDevice(
|
|
1341
|
+
logicalCameraId,
|
|
1342
|
+
"Logical Camera (" + position + ")",
|
|
1343
|
+
position,
|
|
1344
|
+
logicalLenses,
|
|
1345
|
+
minZoom,
|
|
1346
|
+
maxZoom,
|
|
1347
|
+
true
|
|
1348
|
+
)
|
|
1349
|
+
);
|
|
1350
|
+
Log.d(
|
|
1351
|
+
TAG,
|
|
1352
|
+
"Found logical camera: " +
|
|
1353
|
+
logicalCameraId +
|
|
1354
|
+
" (" +
|
|
1355
|
+
position +
|
|
1356
|
+
") with zoom " +
|
|
1357
|
+
minZoom +
|
|
1358
|
+
"-" +
|
|
1359
|
+
maxZoom
|
|
1360
|
+
);
|
|
326
1361
|
|
|
327
|
-
|
|
328
|
-
|
|
1362
|
+
// Get and add physical cameras
|
|
1363
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
1364
|
+
Set<CameraInfo> physicalCameraInfos =
|
|
1365
|
+
cameraInfo.getPhysicalCameraInfos();
|
|
1366
|
+
if (physicalCameraInfos.isEmpty()) continue;
|
|
1367
|
+
|
|
1368
|
+
Log.d(
|
|
1369
|
+
TAG,
|
|
1370
|
+
"Logical camera " +
|
|
1371
|
+
logicalCameraId +
|
|
1372
|
+
" has " +
|
|
1373
|
+
physicalCameraInfos.size() +
|
|
1374
|
+
" physical cameras."
|
|
1375
|
+
);
|
|
1376
|
+
|
|
1377
|
+
for (CameraInfo physicalCameraInfo : physicalCameraInfos) {
|
|
1378
|
+
String physicalId = Camera2CameraInfo.from(
|
|
1379
|
+
physicalCameraInfo
|
|
1380
|
+
).getCameraId();
|
|
1381
|
+
if (physicalId.equals(logicalCameraId)) continue; // Already added as logical
|
|
329
1382
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
1383
|
+
try {
|
|
1384
|
+
CameraCharacteristics characteristics =
|
|
1385
|
+
cameraManager.getCameraCharacteristics(physicalId);
|
|
1386
|
+
String deviceType = "wideAngle";
|
|
1387
|
+
float[] focalLengths = characteristics.get(
|
|
1388
|
+
CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
|
|
1389
|
+
);
|
|
1390
|
+
android.util.SizeF sensorSize = characteristics.get(
|
|
1391
|
+
CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE
|
|
1392
|
+
);
|
|
1393
|
+
|
|
1394
|
+
if (
|
|
1395
|
+
focalLengths != null &&
|
|
1396
|
+
focalLengths.length > 0 &&
|
|
1397
|
+
sensorSize != null &&
|
|
1398
|
+
sensorSize.getWidth() > 0
|
|
1399
|
+
) {
|
|
1400
|
+
double fov =
|
|
1401
|
+
2 *
|
|
1402
|
+
Math.toDegrees(
|
|
1403
|
+
Math.atan(sensorSize.getWidth() / (2 * focalLengths[0]))
|
|
1404
|
+
);
|
|
1405
|
+
if (fov > 90) deviceType = "ultraWide";
|
|
1406
|
+
else if (fov < 40) deviceType = "telephoto";
|
|
1407
|
+
} else if (focalLengths != null && focalLengths.length > 0) {
|
|
1408
|
+
if (focalLengths[0] < 3.0f) deviceType = "ultraWide";
|
|
1409
|
+
else if (focalLengths[0] > 5.0f) deviceType = "telephoto";
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
float physicalMinZoom = 1.0f;
|
|
1413
|
+
float physicalMaxZoom = 1.0f;
|
|
1414
|
+
if (
|
|
1415
|
+
android.os.Build.VERSION.SDK_INT >=
|
|
1416
|
+
android.os.Build.VERSION_CODES.R
|
|
1417
|
+
) {
|
|
1418
|
+
android.util.Range<Float> zoomRange = characteristics.get(
|
|
1419
|
+
CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE
|
|
1420
|
+
);
|
|
1421
|
+
if (zoomRange != null) {
|
|
1422
|
+
physicalMinZoom = zoomRange.getLower();
|
|
1423
|
+
physicalMaxZoom = zoomRange.getUpper();
|
|
340
1424
|
}
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
String label = "Physical " + deviceType + " (" + position + ")";
|
|
1428
|
+
List<LensInfo> physicalLenses = new ArrayList<>();
|
|
1429
|
+
physicalLenses.add(
|
|
1430
|
+
new LensInfo(
|
|
1431
|
+
focalLengths != null ? focalLengths[0] : 4.25f,
|
|
1432
|
+
deviceType,
|
|
1433
|
+
1.0f,
|
|
1434
|
+
physicalMaxZoom
|
|
1435
|
+
)
|
|
1436
|
+
);
|
|
1437
|
+
|
|
1438
|
+
devices.add(
|
|
1439
|
+
new com.ahm.capacitor.camera.preview.model.CameraDevice(
|
|
1440
|
+
physicalId,
|
|
1441
|
+
label,
|
|
1442
|
+
position,
|
|
1443
|
+
physicalLenses,
|
|
1444
|
+
physicalMinZoom,
|
|
1445
|
+
physicalMaxZoom,
|
|
1446
|
+
false
|
|
1447
|
+
)
|
|
1448
|
+
);
|
|
1449
|
+
Log.d(
|
|
1450
|
+
TAG,
|
|
1451
|
+
"Found physical camera: " + physicalId + " (" + label + ")"
|
|
1452
|
+
);
|
|
1453
|
+
} catch (CameraAccessException e) {
|
|
1454
|
+
Log.e(
|
|
1455
|
+
TAG,
|
|
1456
|
+
"Failed to access characteristics for physical camera " +
|
|
1457
|
+
physicalId,
|
|
1458
|
+
e
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return devices;
|
|
1465
|
+
} catch (Exception e) {
|
|
1466
|
+
Log.e(TAG, "getAvailableDevicesStatic: Error getting devices", e);
|
|
1467
|
+
return Collections.emptyList();
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
public static ZoomFactors getZoomFactorsStatic() {
|
|
1472
|
+
try {
|
|
1473
|
+
// For static method, return default zoom factors
|
|
1474
|
+
// We can try to detect if ultra-wide is available by checking device list
|
|
1475
|
+
|
|
1476
|
+
float minZoom = 1.0f;
|
|
1477
|
+
float maxZoom = 10.0f;
|
|
1478
|
+
|
|
1479
|
+
Log.d(
|
|
1480
|
+
TAG,
|
|
1481
|
+
"getZoomFactorsStatic: Final range - minZoom: " +
|
|
1482
|
+
minZoom +
|
|
1483
|
+
", maxZoom: " +
|
|
1484
|
+
maxZoom
|
|
1485
|
+
);
|
|
1486
|
+
LensInfo defaultLens = new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
|
|
1487
|
+
return new ZoomFactors(minZoom, maxZoom, 1.0f, defaultLens);
|
|
1488
|
+
} catch (Exception e) {
|
|
1489
|
+
Log.e(TAG, "getZoomFactorsStatic: Error getting zoom factors", e);
|
|
1490
|
+
LensInfo defaultLens = new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
|
|
1491
|
+
return new ZoomFactors(1.0f, 10.0f, 1.0f, defaultLens);
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
341
1494
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
ExifInterface exifInterface = new ExifInterface(tempFile.getAbsolutePath());
|
|
347
|
-
JSONObject exifData = getExifData(exifInterface);
|
|
348
|
-
|
|
349
|
-
if (saveToGallery) {
|
|
350
|
-
saveImageToGallery(bytes);
|
|
351
|
-
}
|
|
1495
|
+
public ZoomFactors getZoomFactors() {
|
|
1496
|
+
if (camera == null) {
|
|
1497
|
+
return getZoomFactorsStatic();
|
|
1498
|
+
}
|
|
352
1499
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
1500
|
+
try {
|
|
1501
|
+
// Get the current zoom from active camera
|
|
1502
|
+
float currentZoom = Objects.requireNonNull(
|
|
1503
|
+
camera.getCameraInfo().getZoomState().getValue()
|
|
1504
|
+
).getZoomRatio();
|
|
1505
|
+
float minZoom = camera
|
|
1506
|
+
.getCameraInfo()
|
|
1507
|
+
.getZoomState()
|
|
1508
|
+
.getValue()
|
|
1509
|
+
.getMinZoomRatio();
|
|
1510
|
+
float maxZoom = camera
|
|
1511
|
+
.getCameraInfo()
|
|
1512
|
+
.getZoomState()
|
|
1513
|
+
.getValue()
|
|
1514
|
+
.getMaxZoomRatio();
|
|
1515
|
+
|
|
1516
|
+
Log.d(
|
|
1517
|
+
TAG,
|
|
1518
|
+
"getZoomFactors: Combined range - minZoom: " +
|
|
1519
|
+
minZoom +
|
|
1520
|
+
", maxZoom: " +
|
|
1521
|
+
maxZoom +
|
|
1522
|
+
", currentZoom: " +
|
|
1523
|
+
currentZoom
|
|
1524
|
+
);
|
|
1525
|
+
|
|
1526
|
+
return new ZoomFactors(
|
|
1527
|
+
minZoom,
|
|
1528
|
+
maxZoom,
|
|
1529
|
+
currentZoom,
|
|
1530
|
+
getCurrentLensInfo()
|
|
1531
|
+
);
|
|
1532
|
+
} catch (Exception e) {
|
|
1533
|
+
Log.e(TAG, "getZoomFactors: Error getting zoom factors", e);
|
|
1534
|
+
return new ZoomFactors(1.0f, 1.0f, 1.0f, getCurrentLensInfo());
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
356
1537
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
} catch (Exception e) {
|
|
361
|
-
Log.e(TAG, "capturePhoto: Error processing image", e);
|
|
362
|
-
if (listener != null) {
|
|
363
|
-
listener.onPictureTakenError("Error processing image: " + e.getMessage());
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
);
|
|
1538
|
+
private LensInfo getCurrentLensInfo() {
|
|
1539
|
+
if (camera == null) {
|
|
1540
|
+
return new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
|
|
369
1541
|
}
|
|
370
1542
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
for (String[] tag : EXIF_TAGS) {
|
|
376
|
-
String value = exifInterface.getAttribute(tag[0]);
|
|
377
|
-
if (value != null) {
|
|
378
|
-
exifData.put(tag[1], value);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
} catch (Exception e) {
|
|
382
|
-
Log.e(TAG, "getExifData: Error reading exif data", e);
|
|
383
|
-
}
|
|
384
|
-
return exifData;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
private static final String[][] EXIF_TAGS = new String[][]{
|
|
388
|
-
{ExifInterface.TAG_APERTURE_VALUE, "ApertureValue"},
|
|
389
|
-
{ExifInterface.TAG_ARTIST, "Artist"},
|
|
390
|
-
{ExifInterface.TAG_BITS_PER_SAMPLE, "BitsPerSample"},
|
|
391
|
-
{ExifInterface.TAG_BRIGHTNESS_VALUE, "BrightnessValue"},
|
|
392
|
-
{ExifInterface.TAG_CFA_PATTERN, "CFAPattern"},
|
|
393
|
-
{ExifInterface.TAG_COLOR_SPACE, "ColorSpace"},
|
|
394
|
-
{ExifInterface.TAG_COMPONENTS_CONFIGURATION, "ComponentsConfiguration"},
|
|
395
|
-
{ExifInterface.TAG_COMPRESSED_BITS_PER_PIXEL, "CompressedBitsPerPixel"},
|
|
396
|
-
{ExifInterface.TAG_COMPRESSION, "Compression"},
|
|
397
|
-
{ExifInterface.TAG_CONTRAST, "Contrast"},
|
|
398
|
-
{ExifInterface.TAG_COPYRIGHT, "Copyright"},
|
|
399
|
-
{ExifInterface.TAG_CUSTOM_RENDERED, "CustomRendered"},
|
|
400
|
-
{ExifInterface.TAG_DATETIME, "DateTime"},
|
|
401
|
-
{ExifInterface.TAG_DATETIME_DIGITIZED, "DateTimeDigitized"},
|
|
402
|
-
{ExifInterface.TAG_DATETIME_ORIGINAL, "DateTimeOriginal"},
|
|
403
|
-
{ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION, "DeviceSettingDescription"},
|
|
404
|
-
{ExifInterface.TAG_DIGITAL_ZOOM_RATIO, "DigitalZoomRatio"},
|
|
405
|
-
{ExifInterface.TAG_DNG_VERSION, "DNGVersion"},
|
|
406
|
-
{ExifInterface.TAG_EXIF_VERSION, "ExifVersion"},
|
|
407
|
-
{ExifInterface.TAG_EXPOSURE_BIAS_VALUE, "ExposureBiasValue"},
|
|
408
|
-
{ExifInterface.TAG_EXPOSURE_INDEX, "ExposureIndex"},
|
|
409
|
-
{ExifInterface.TAG_EXPOSURE_MODE, "ExposureMode"},
|
|
410
|
-
{ExifInterface.TAG_EXPOSURE_PROGRAM, "ExposureProgram"},
|
|
411
|
-
{ExifInterface.TAG_EXPOSURE_TIME, "ExposureTime"},
|
|
412
|
-
{ExifInterface.TAG_FILE_SOURCE, "FileSource"},
|
|
413
|
-
{ExifInterface.TAG_FLASH, "Flash"},
|
|
414
|
-
{ExifInterface.TAG_FLASHPIX_VERSION, "FlashpixVersion"},
|
|
415
|
-
{ExifInterface.TAG_FLASH_ENERGY, "FlashEnergy"},
|
|
416
|
-
{ExifInterface.TAG_FOCAL_LENGTH, "FocalLength"},
|
|
417
|
-
{ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM, "FocalLengthIn35mmFilm"},
|
|
418
|
-
{ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT, "FocalPlaneResolutionUnit"},
|
|
419
|
-
{ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION, "FocalPlaneXResolution"},
|
|
420
|
-
{ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION, "FocalPlaneYResolution"},
|
|
421
|
-
{ExifInterface.TAG_F_NUMBER, "FNumber"},
|
|
422
|
-
{ExifInterface.TAG_GAIN_CONTROL, "GainControl"},
|
|
423
|
-
{ExifInterface.TAG_GPS_ALTITUDE, "GPSAltitude"},
|
|
424
|
-
{ExifInterface.TAG_GPS_ALTITUDE_REF, "GPSAltitudeRef"},
|
|
425
|
-
{ExifInterface.TAG_GPS_AREA_INFORMATION, "GPSAreaInformation"},
|
|
426
|
-
{ExifInterface.TAG_GPS_DATESTAMP, "GPSDateStamp"},
|
|
427
|
-
{ExifInterface.TAG_GPS_DEST_BEARING, "GPSDestBearing"},
|
|
428
|
-
{ExifInterface.TAG_GPS_DEST_BEARING_REF, "GPSDestBearingRef"},
|
|
429
|
-
{ExifInterface.TAG_GPS_DEST_DISTANCE, "GPSDestDistance"},
|
|
430
|
-
{ExifInterface.TAG_GPS_DEST_DISTANCE_REF, "GPSDestDistanceRef"},
|
|
431
|
-
{ExifInterface.TAG_GPS_DEST_LATITUDE, "GPSDestLatitude"},
|
|
432
|
-
{ExifInterface.TAG_GPS_DEST_LATITUDE_REF, "GPSDestLatitudeRef"},
|
|
433
|
-
{ExifInterface.TAG_GPS_DEST_LONGITUDE, "GPSDestLongitude"},
|
|
434
|
-
{ExifInterface.TAG_GPS_DEST_LONGITUDE_REF, "GPSDestLongitudeRef"},
|
|
435
|
-
{ExifInterface.TAG_GPS_DIFFERENTIAL, "GPSDifferential"},
|
|
436
|
-
{ExifInterface.TAG_GPS_DOP, "GPSDOP"},
|
|
437
|
-
{ExifInterface.TAG_GPS_IMG_DIRECTION, "GPSImgDirection"},
|
|
438
|
-
{ExifInterface.TAG_GPS_IMG_DIRECTION_REF, "GPSImgDirectionRef"},
|
|
439
|
-
{ExifInterface.TAG_GPS_LATITUDE, "GPSLatitude"},
|
|
440
|
-
{ExifInterface.TAG_GPS_LATITUDE_REF, "GPSLatitudeRef"},
|
|
441
|
-
{ExifInterface.TAG_GPS_LONGITUDE, "GPSLongitude"},
|
|
442
|
-
{ExifInterface.TAG_GPS_LONGITUDE_REF, "GPSLongitudeRef"},
|
|
443
|
-
{ExifInterface.TAG_GPS_MAP_DATUM, "GPSMapDatum"},
|
|
444
|
-
{ExifInterface.TAG_GPS_MEASURE_MODE, "GPSMeasureMode"},
|
|
445
|
-
{ExifInterface.TAG_GPS_PROCESSING_METHOD, "GPSProcessingMethod"},
|
|
446
|
-
{ExifInterface.TAG_GPS_SATELLITES, "GPSSatellites"},
|
|
447
|
-
{ExifInterface.TAG_GPS_SPEED, "GPSSpeed"},
|
|
448
|
-
{ExifInterface.TAG_GPS_SPEED_REF, "GPSSpeedRef"},
|
|
449
|
-
{ExifInterface.TAG_GPS_STATUS, "GPSStatus"},
|
|
450
|
-
{ExifInterface.TAG_GPS_TIMESTAMP, "GPSTimeStamp"},
|
|
451
|
-
{ExifInterface.TAG_GPS_TRACK, "GPSTrack"},
|
|
452
|
-
{ExifInterface.TAG_GPS_TRACK_REF, "GPSTrackRef"},
|
|
453
|
-
{ExifInterface.TAG_GPS_VERSION_ID, "GPSVersionID"},
|
|
454
|
-
{ExifInterface.TAG_IMAGE_DESCRIPTION, "ImageDescription"},
|
|
455
|
-
{ExifInterface.TAG_IMAGE_LENGTH, "ImageLength"},
|
|
456
|
-
{ExifInterface.TAG_IMAGE_UNIQUE_ID, "ImageUniqueID"},
|
|
457
|
-
{ExifInterface.TAG_IMAGE_WIDTH, "ImageWidth"},
|
|
458
|
-
{ExifInterface.TAG_INTEROPERABILITY_INDEX, "InteroperabilityIndex"},
|
|
459
|
-
{ExifInterface.TAG_ISO_SPEED, "ISOSpeed"},
|
|
460
|
-
{ExifInterface.TAG_ISO_SPEED_LATITUDE_YYY, "ISOSpeedLatitudeyyy"},
|
|
461
|
-
{ExifInterface.TAG_ISO_SPEED_LATITUDE_ZZZ, "ISOSpeedLatitudezzz"},
|
|
462
|
-
{ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT, "JPEGInterchangeFormat"},
|
|
463
|
-
{ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, "JPEGInterchangeFormatLength"},
|
|
464
|
-
{ExifInterface.TAG_LIGHT_SOURCE, "LightSource"},
|
|
465
|
-
{ExifInterface.TAG_MAKE, "Make"},
|
|
466
|
-
{ExifInterface.TAG_MAKER_NOTE, "MakerNote"},
|
|
467
|
-
{ExifInterface.TAG_MAX_APERTURE_VALUE, "MaxApertureValue"},
|
|
468
|
-
{ExifInterface.TAG_METERING_MODE, "MeteringMode"},
|
|
469
|
-
{ExifInterface.TAG_MODEL, "Model"},
|
|
470
|
-
{ExifInterface.TAG_NEW_SUBFILE_TYPE, "NewSubfileType"},
|
|
471
|
-
{ExifInterface.TAG_OECF, "OECF"},
|
|
472
|
-
{ExifInterface.TAG_OFFSET_TIME, "OffsetTime"},
|
|
473
|
-
{ExifInterface.TAG_OFFSET_TIME_DIGITIZED, "OffsetTimeDigitized"},
|
|
474
|
-
{ExifInterface.TAG_OFFSET_TIME_ORIGINAL, "OffsetTimeOriginal"},
|
|
475
|
-
{ExifInterface.TAG_ORF_ASPECT_FRAME, "ORFAspectFrame"},
|
|
476
|
-
{ExifInterface.TAG_ORF_PREVIEW_IMAGE_LENGTH, "ORFPreviewImageLength"},
|
|
477
|
-
{ExifInterface.TAG_ORF_PREVIEW_IMAGE_START, "ORFPreviewImageStart"},
|
|
478
|
-
{ExifInterface.TAG_ORF_THUMBNAIL_IMAGE, "ORFThumbnailImage"},
|
|
479
|
-
{ExifInterface.TAG_ORIENTATION, "Orientation"},
|
|
480
|
-
{ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION, "PhotometricInterpretation"},
|
|
481
|
-
{ExifInterface.TAG_PIXEL_X_DIMENSION, "PixelXDimension"},
|
|
482
|
-
{ExifInterface.TAG_PIXEL_Y_DIMENSION, "PixelYDimension"},
|
|
483
|
-
{ExifInterface.TAG_PLANAR_CONFIGURATION, "PlanarConfiguration"},
|
|
484
|
-
{ExifInterface.TAG_PRIMARY_CHROMATICITIES, "PrimaryChromaticities"},
|
|
485
|
-
{ExifInterface.TAG_RECOMMENDED_EXPOSURE_INDEX, "RecommendedExposureIndex"},
|
|
486
|
-
{ExifInterface.TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite"},
|
|
487
|
-
{ExifInterface.TAG_RELATED_SOUND_FILE, "RelatedSoundFile"},
|
|
488
|
-
{ExifInterface.TAG_RESOLUTION_UNIT, "ResolutionUnit"},
|
|
489
|
-
{ExifInterface.TAG_ROWS_PER_STRIP, "RowsPerStrip"},
|
|
490
|
-
{ExifInterface.TAG_RW2_ISO, "RW2ISO"},
|
|
491
|
-
{ExifInterface.TAG_RW2_JPG_FROM_RAW, "RW2JpgFromRaw"},
|
|
492
|
-
{ExifInterface.TAG_RW2_SENSOR_BOTTOM_BORDER, "RW2SensorBottomBorder"},
|
|
493
|
-
{ExifInterface.TAG_RW2_SENSOR_LEFT_BORDER, "RW2SensorLeftBorder"},
|
|
494
|
-
{ExifInterface.TAG_RW2_SENSOR_RIGHT_BORDER, "RW2SensorRightBorder"},
|
|
495
|
-
{ExifInterface.TAG_RW2_SENSOR_TOP_BORDER, "RW2SensorTopBorder"},
|
|
496
|
-
{ExifInterface.TAG_SAMPLES_PER_PIXEL, "SamplesPerPixel"},
|
|
497
|
-
{ExifInterface.TAG_SATURATION, "Saturation"},
|
|
498
|
-
{ExifInterface.TAG_SCENE_CAPTURE_TYPE, "SceneCaptureType"},
|
|
499
|
-
{ExifInterface.TAG_SCENE_TYPE, "SceneType"},
|
|
500
|
-
{ExifInterface.TAG_SENSING_METHOD, "SensingMethod"},
|
|
501
|
-
{ExifInterface.TAG_SENSITIVITY_TYPE, "SensitivityType"},
|
|
502
|
-
{ExifInterface.TAG_SHARPNESS, "Sharpness"},
|
|
503
|
-
{ExifInterface.TAG_SHUTTER_SPEED_VALUE, "ShutterSpeedValue"},
|
|
504
|
-
{ExifInterface.TAG_SOFTWARE, "Software"},
|
|
505
|
-
{ExifInterface.TAG_SPATIAL_FREQUENCY_RESPONSE, "SpatialFrequencyResponse"},
|
|
506
|
-
{ExifInterface.TAG_SPECTRAL_SENSITIVITY, "SpectralSensitivity"},
|
|
507
|
-
{ExifInterface.TAG_STANDARD_OUTPUT_SENSITIVITY, "StandardOutputSensitivity"},
|
|
508
|
-
{ExifInterface.TAG_STRIP_BYTE_COUNTS, "StripByteCounts"},
|
|
509
|
-
{ExifInterface.TAG_STRIP_OFFSETS, "StripOffsets"},
|
|
510
|
-
{ExifInterface.TAG_SUBFILE_TYPE, "SubfileType"},
|
|
511
|
-
{ExifInterface.TAG_SUBJECT_AREA, "SubjectArea"},
|
|
512
|
-
{ExifInterface.TAG_SUBJECT_DISTANCE, "SubjectDistance"},
|
|
513
|
-
{ExifInterface.TAG_SUBJECT_DISTANCE_RANGE, "SubjectDistanceRange"},
|
|
514
|
-
{ExifInterface.TAG_SUBJECT_LOCATION, "SubjectLocation"},
|
|
515
|
-
{ExifInterface.TAG_SUBSEC_TIME, "SubSecTime"},
|
|
516
|
-
{ExifInterface.TAG_SUBSEC_TIME_DIGITIZED, "SubSecTimeDigitized"},
|
|
517
|
-
{ExifInterface.TAG_SUBSEC_TIME_ORIGINAL, "SubSecTimeOriginal"},
|
|
518
|
-
{ExifInterface.TAG_THUMBNAIL_IMAGE_LENGTH, "ThumbnailImageLength"},
|
|
519
|
-
{ExifInterface.TAG_THUMBNAIL_IMAGE_WIDTH, "ThumbnailImageWidth"},
|
|
520
|
-
{ExifInterface.TAG_TRANSFER_FUNCTION, "TransferFunction"},
|
|
521
|
-
{ExifInterface.TAG_USER_COMMENT, "UserComment"},
|
|
522
|
-
{ExifInterface.TAG_WHITE_BALANCE, "WhiteBalance"},
|
|
523
|
-
{ExifInterface.TAG_WHITE_POINT, "WhitePoint"},
|
|
524
|
-
{ExifInterface.TAG_X_RESOLUTION, "XResolution"},
|
|
525
|
-
{ExifInterface.TAG_Y_CB_CR_COEFFICIENTS, "YCbCrCoefficients"},
|
|
526
|
-
{ExifInterface.TAG_Y_CB_CR_POSITIONING, "YCbCrPositioning"},
|
|
527
|
-
{ExifInterface.TAG_Y_CB_CR_SUB_SAMPLING, "YCbCrSubSampling"},
|
|
528
|
-
{ExifInterface.TAG_Y_RESOLUTION, "YResolution"}
|
|
529
|
-
};
|
|
1543
|
+
try {
|
|
1544
|
+
float currentZoom = Objects.requireNonNull(
|
|
1545
|
+
camera.getCameraInfo().getZoomState().getValue()
|
|
1546
|
+
).getZoomRatio();
|
|
530
1547
|
|
|
531
|
-
|
|
532
|
-
|
|
1548
|
+
// Determine device type based on zoom capabilities
|
|
1549
|
+
String deviceType = "wideAngle";
|
|
1550
|
+
float baseZoomRatio = 1.0f;
|
|
533
1551
|
|
|
534
|
-
|
|
535
|
-
if (listener != null) {
|
|
536
|
-
listener.onSampleTakenError("Camera not ready");
|
|
537
|
-
}
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
1552
|
+
float digitalZoom = currentZoom / baseZoomRatio;
|
|
540
1553
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
if (listener != null) {
|
|
548
|
-
listener.onSampleTakenError("Sample capture failed: " + exception.getMessage());
|
|
549
|
-
}
|
|
550
|
-
}
|
|
1554
|
+
return new LensInfo(4.25f, deviceType, baseZoomRatio, digitalZoom);
|
|
1555
|
+
} catch (Exception e) {
|
|
1556
|
+
Log.e(TAG, "getCurrentLensInfo: Error getting lens info", e);
|
|
1557
|
+
return new LensInfo(4.25f, "wideAngle", 1.0f, 1.0f);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
551
1560
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
1561
|
+
public void setZoom(float zoomRatio, boolean autoFocus) throws Exception {
|
|
1562
|
+
if (camera == null) {
|
|
1563
|
+
throw new Exception("Camera not initialized");
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
Log.d(TAG, "setZoom: Requested zoom ratio: " + zoomRatio);
|
|
1567
|
+
|
|
1568
|
+
// Just let CameraX handle everything - it should automatically switch lenses
|
|
1569
|
+
try {
|
|
1570
|
+
ListenableFuture<Void> zoomFuture = camera
|
|
1571
|
+
.getCameraControl()
|
|
1572
|
+
.setZoomRatio(zoomRatio);
|
|
1573
|
+
|
|
1574
|
+
// Add callback to see what actually happened
|
|
1575
|
+
zoomFuture.addListener(
|
|
1576
|
+
() -> {
|
|
1577
|
+
try {
|
|
1578
|
+
zoomFuture.get();
|
|
1579
|
+
Log.d(TAG, "Zoom successfully set to " + zoomRatio);
|
|
1580
|
+
// Trigger autofocus after zoom if requested
|
|
1581
|
+
if (autoFocus) {
|
|
1582
|
+
triggerAutoFocus();
|
|
571
1583
|
}
|
|
572
|
-
|
|
1584
|
+
} catch (Exception e) {
|
|
1585
|
+
Log.e(TAG, "Error setting zoom: " + e.getMessage());
|
|
1586
|
+
}
|
|
1587
|
+
},
|
|
1588
|
+
ContextCompat.getMainExecutor(context)
|
|
1589
|
+
);
|
|
1590
|
+
} catch (Exception e) {
|
|
1591
|
+
Log.e(TAG, "Failed to set zoom: " + e.getMessage());
|
|
1592
|
+
throw e;
|
|
573
1593
|
}
|
|
1594
|
+
}
|
|
574
1595
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
byte[] bytes = new byte[buffer.remaining()];
|
|
579
|
-
buffer.get(bytes);
|
|
580
|
-
return bytes;
|
|
1596
|
+
public void setFocus(float x, float y) throws Exception {
|
|
1597
|
+
if (camera == null) {
|
|
1598
|
+
throw new Exception("Camera not initialized");
|
|
581
1599
|
}
|
|
582
1600
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
public static List<com.ahm.capacitor.camera.preview.model.CameraDevice> getAvailableDevicesStatic(Context context) {
|
|
586
|
-
Log.d(TAG, "getAvailableDevicesStatic: Starting CameraX device enumeration with getPhysicalCameraInfos.");
|
|
587
|
-
List<com.ahm.capacitor.camera.preview.model.CameraDevice> devices = new ArrayList<>();
|
|
588
|
-
try {
|
|
589
|
-
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
|
|
590
|
-
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
|
|
591
|
-
CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
|
|
592
|
-
|
|
593
|
-
for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
|
|
594
|
-
String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
|
|
595
|
-
String position = isBackCamera(cameraInfo) ? "rear" : "front";
|
|
596
|
-
|
|
597
|
-
// Add logical camera
|
|
598
|
-
float minZoom = Objects.requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio();
|
|
599
|
-
float maxZoom = cameraInfo.getZoomState().getValue().getMaxZoomRatio();
|
|
600
|
-
List<LensInfo> logicalLenses = new ArrayList<>();
|
|
601
|
-
logicalLenses.add(new LensInfo(4.25f, "wideAngle", 1.0f, maxZoom));
|
|
602
|
-
devices.add(new com.ahm.capacitor.camera.preview.model.CameraDevice(
|
|
603
|
-
logicalCameraId, "Logical Camera (" + position + ")", position, logicalLenses, minZoom, maxZoom, true
|
|
604
|
-
));
|
|
605
|
-
Log.d(TAG, "Found logical camera: " + logicalCameraId + " (" + position + ") with zoom " + minZoom + "-" + maxZoom);
|
|
606
|
-
|
|
607
|
-
// Get and add physical cameras
|
|
608
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
609
|
-
Set<CameraInfo> physicalCameraInfos = cameraInfo.getPhysicalCameraInfos();
|
|
610
|
-
if (physicalCameraInfos.isEmpty()) continue;
|
|
611
|
-
|
|
612
|
-
Log.d(TAG, "Logical camera " + logicalCameraId + " has " + physicalCameraInfos.size() + " physical cameras.");
|
|
613
|
-
|
|
614
|
-
for (CameraInfo physicalCameraInfo : physicalCameraInfos) {
|
|
615
|
-
String physicalId = Camera2CameraInfo.from(physicalCameraInfo).getCameraId();
|
|
616
|
-
if (physicalId.equals(logicalCameraId)) continue; // Already added as logical
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(physicalId);
|
|
620
|
-
String deviceType = "wideAngle";
|
|
621
|
-
float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
|
|
622
|
-
android.util.SizeF sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
|
|
623
|
-
|
|
624
|
-
if (focalLengths != null && focalLengths.length > 0 && sensorSize != null && sensorSize.getWidth() > 0) {
|
|
625
|
-
double fov = 2 * Math.toDegrees(Math.atan(sensorSize.getWidth() / (2 * focalLengths[0])));
|
|
626
|
-
if (fov > 90) deviceType = "ultraWide";
|
|
627
|
-
else if (fov < 40) deviceType = "telephoto";
|
|
628
|
-
} else if (focalLengths != null && focalLengths.length > 0) {
|
|
629
|
-
if (focalLengths[0] < 3.0f) deviceType = "ultraWide";
|
|
630
|
-
else if (focalLengths[0] > 5.0f) deviceType = "telephoto";
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
float physicalMinZoom = 1.0f;
|
|
634
|
-
float physicalMaxZoom = 1.0f;
|
|
635
|
-
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
|
636
|
-
android.util.Range<Float> zoomRange = characteristics.get(CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE);
|
|
637
|
-
if (zoomRange != null) {
|
|
638
|
-
physicalMinZoom = zoomRange.getLower();
|
|
639
|
-
physicalMaxZoom = zoomRange.getUpper();
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
String label = "Physical " + deviceType + " (" + position + ")";
|
|
644
|
-
List<LensInfo> physicalLenses = new ArrayList<>();
|
|
645
|
-
physicalLenses.add(new LensInfo(focalLengths != null ? focalLengths[0] : 4.25f, deviceType, 1.0f, physicalMaxZoom));
|
|
646
|
-
|
|
647
|
-
devices.add(new com.ahm.capacitor.camera.preview.model.CameraDevice(
|
|
648
|
-
physicalId, label, position, physicalLenses, physicalMinZoom, physicalMaxZoom, false
|
|
649
|
-
));
|
|
650
|
-
Log.d(TAG, "Found physical camera: " + physicalId + " (" + label + ")");
|
|
651
|
-
} catch (CameraAccessException e) {
|
|
652
|
-
Log.e(TAG, "Failed to access characteristics for physical camera " + physicalId, e);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return devices;
|
|
658
|
-
} catch (Exception e) {
|
|
659
|
-
Log.e(TAG, "getAvailableDevicesStatic: Error getting devices", e);
|
|
660
|
-
return Collections.emptyList();
|
|
661
|
-
}
|
|
1601
|
+
if (previewView == null) {
|
|
1602
|
+
throw new Exception("Preview view not initialized");
|
|
662
1603
|
}
|
|
663
1604
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1605
|
+
// Validate that coordinates are within bounds (0-1 range)
|
|
1606
|
+
if (x < 0f || x > 1f || y < 0f || y > 1f) {
|
|
1607
|
+
Log.w(TAG, "setFocus: Coordinates out of bounds - x: " + x + ", y: " + y);
|
|
1608
|
+
throw new Exception("Focus coordinates must be between 0 and 1");
|
|
1609
|
+
}
|
|
668
1610
|
|
|
669
|
-
|
|
670
|
-
|
|
1611
|
+
// Cancel any ongoing focus operation
|
|
1612
|
+
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
1613
|
+
Log.d(TAG, "setFocus: Cancelling previous focus operation");
|
|
1614
|
+
currentFocusFuture.cancel(true);
|
|
1615
|
+
}
|
|
671
1616
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
1617
|
+
int viewWidth = previewView.getWidth();
|
|
1618
|
+
int viewHeight = previewView.getHeight();
|
|
1619
|
+
|
|
1620
|
+
if (viewWidth <= 0 || viewHeight <= 0) {
|
|
1621
|
+
throw new Exception(
|
|
1622
|
+
"Preview view has invalid dimensions: " + viewWidth + "x" + viewHeight
|
|
1623
|
+
);
|
|
680
1624
|
}
|
|
681
1625
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1626
|
+
// Only show focus indicator after validation passes
|
|
1627
|
+
float indicatorX = x * viewWidth;
|
|
1628
|
+
float indicatorY = y * viewHeight;
|
|
1629
|
+
showFocusIndicator(indicatorX, indicatorY);
|
|
1630
|
+
|
|
1631
|
+
// Create MeteringPoint using the preview view
|
|
1632
|
+
MeteringPointFactory factory = previewView.getMeteringPointFactory();
|
|
1633
|
+
MeteringPoint point = factory.createPoint(x * viewWidth, y * viewHeight);
|
|
1634
|
+
|
|
1635
|
+
// Create focus and metering action
|
|
1636
|
+
FocusMeteringAction action = new FocusMeteringAction.Builder(
|
|
1637
|
+
point,
|
|
1638
|
+
FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
|
|
1639
|
+
)
|
|
1640
|
+
.setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
|
|
1641
|
+
.build();
|
|
1642
|
+
|
|
1643
|
+
try {
|
|
1644
|
+
currentFocusFuture = camera
|
|
1645
|
+
.getCameraControl()
|
|
1646
|
+
.startFocusAndMetering(action);
|
|
1647
|
+
|
|
1648
|
+
currentFocusFuture.addListener(
|
|
1649
|
+
() -> {
|
|
1650
|
+
try {
|
|
1651
|
+
FocusMeteringResult result = currentFocusFuture.get();
|
|
1652
|
+
} catch (Exception e) {
|
|
1653
|
+
// Handle cancellation gracefully - this is expected when rapid taps occur
|
|
1654
|
+
if (
|
|
1655
|
+
e.getMessage() != null &&
|
|
1656
|
+
(e
|
|
1657
|
+
.getMessage()
|
|
1658
|
+
.contains("Cancelled by another startFocusAndMetering") ||
|
|
1659
|
+
e.getMessage().contains("OperationCanceledException") ||
|
|
1660
|
+
e
|
|
1661
|
+
.getClass()
|
|
1662
|
+
.getSimpleName()
|
|
1663
|
+
.contains("OperationCanceledException"))
|
|
1664
|
+
) {
|
|
1665
|
+
Log.d(
|
|
1666
|
+
TAG,
|
|
1667
|
+
"Focus operation was cancelled by a newer focus request"
|
|
1668
|
+
);
|
|
1669
|
+
} else {
|
|
1670
|
+
Log.e(TAG, "Error during focus: " + e.getMessage());
|
|
1671
|
+
}
|
|
1672
|
+
} finally {
|
|
1673
|
+
// Clear the reference if this is still the current operation
|
|
1674
|
+
if (currentFocusFuture != null && currentFocusFuture.isDone()) {
|
|
1675
|
+
currentFocusFuture = null;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
},
|
|
1679
|
+
ContextCompat.getMainExecutor(context)
|
|
1680
|
+
);
|
|
1681
|
+
} catch (Exception e) {
|
|
1682
|
+
currentFocusFuture = null;
|
|
1683
|
+
Log.e(TAG, "Failed to set focus: " + e.getMessage());
|
|
1684
|
+
throw e;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
686
1687
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
1688
|
+
private void showFocusIndicator(float x, float y) {
|
|
1689
|
+
if (previewContainer == null) {
|
|
1690
|
+
Log.w(TAG, "showFocusIndicator: previewContainer is null");
|
|
1691
|
+
return;
|
|
1692
|
+
}
|
|
692
1693
|
|
|
693
|
-
|
|
1694
|
+
// Check if container has been laid out
|
|
1695
|
+
if (previewContainer.getWidth() == 0 || previewContainer.getHeight() == 0) {
|
|
1696
|
+
Log.w(
|
|
1697
|
+
TAG,
|
|
1698
|
+
"showFocusIndicator: previewContainer not laid out yet, posting to run after layout"
|
|
1699
|
+
);
|
|
1700
|
+
previewContainer.post(() -> showFocusIndicator(x, y));
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
694
1703
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
1704
|
+
// Remove any existing focus indicator
|
|
1705
|
+
if (focusIndicatorView != null) {
|
|
1706
|
+
previewContainer.removeView(focusIndicatorView);
|
|
1707
|
+
focusIndicatorView = null;
|
|
700
1708
|
}
|
|
701
1709
|
|
|
1710
|
+
// Create an elegant focus indicator
|
|
1711
|
+
View container = new View(context);
|
|
1712
|
+
int size = (int) (60 * context.getResources().getDisplayMetrics().density); // 60dp size
|
|
1713
|
+
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(size, size);
|
|
1714
|
+
|
|
1715
|
+
// Center the indicator on the touch point with bounds checking
|
|
1716
|
+
int containerWidth = previewContainer.getWidth();
|
|
1717
|
+
int containerHeight = previewContainer.getHeight();
|
|
1718
|
+
|
|
1719
|
+
params.leftMargin = Math.max(
|
|
1720
|
+
0,
|
|
1721
|
+
Math.min((int) (x - size / 2), containerWidth - size)
|
|
1722
|
+
);
|
|
1723
|
+
params.topMargin = Math.max(
|
|
1724
|
+
0,
|
|
1725
|
+
Math.min((int) (y - size / 2), containerHeight - size)
|
|
1726
|
+
);
|
|
1727
|
+
|
|
1728
|
+
// Create an elegant focus ring - white stroke with transparent center
|
|
1729
|
+
GradientDrawable drawable = new GradientDrawable();
|
|
1730
|
+
drawable.setShape(GradientDrawable.OVAL);
|
|
1731
|
+
drawable.setStroke(
|
|
1732
|
+
(int) (2 * context.getResources().getDisplayMetrics().density),
|
|
1733
|
+
Color.WHITE
|
|
1734
|
+
); // 2dp white stroke
|
|
1735
|
+
drawable.setColor(Color.TRANSPARENT); // Transparent center
|
|
1736
|
+
container.setBackground(drawable);
|
|
1737
|
+
|
|
1738
|
+
focusIndicatorView = container;
|
|
1739
|
+
|
|
1740
|
+
// Set initial state for smooth animation
|
|
1741
|
+
focusIndicatorView.setAlpha(1f); // Start visible
|
|
1742
|
+
focusIndicatorView.setScaleX(1.8f); // Start larger for scale-in effect
|
|
1743
|
+
focusIndicatorView.setScaleY(1.8f);
|
|
1744
|
+
focusIndicatorView.setVisibility(View.VISIBLE);
|
|
1745
|
+
|
|
1746
|
+
// Ensure container doesn't intercept touch events
|
|
1747
|
+
container.setClickable(false);
|
|
1748
|
+
container.setFocusable(false);
|
|
1749
|
+
|
|
1750
|
+
// Ensure the focus indicator has a high elevation for visibility
|
|
1751
|
+
if (
|
|
1752
|
+
android.os.Build.VERSION.SDK_INT >=
|
|
1753
|
+
android.os.Build.VERSION_CODES.LOLLIPOP
|
|
1754
|
+
) {
|
|
1755
|
+
focusIndicatorView.setElevation(10f);
|
|
1756
|
+
}
|
|
702
1757
|
|
|
1758
|
+
// Add to container first
|
|
1759
|
+
previewContainer.addView(focusIndicatorView, params);
|
|
1760
|
+
|
|
1761
|
+
// Fix z-ordering: ensure focus indicator is always on top
|
|
1762
|
+
focusIndicatorView.bringToFront();
|
|
1763
|
+
|
|
1764
|
+
// Force a layout pass to ensure the view is properly positioned
|
|
1765
|
+
previewContainer.requestLayout();
|
|
1766
|
+
|
|
1767
|
+
// Smooth scale down animation with easing (no fade needed since we start visible)
|
|
1768
|
+
ScaleAnimation scaleAnimation = new ScaleAnimation(
|
|
1769
|
+
1.8f,
|
|
1770
|
+
1.0f,
|
|
1771
|
+
1.8f,
|
|
1772
|
+
1.0f,
|
|
1773
|
+
Animation.RELATIVE_TO_SELF,
|
|
1774
|
+
0.5f,
|
|
1775
|
+
Animation.RELATIVE_TO_SELF,
|
|
1776
|
+
0.5f
|
|
1777
|
+
);
|
|
1778
|
+
scaleAnimation.setDuration(300);
|
|
1779
|
+
scaleAnimation.setInterpolator(
|
|
1780
|
+
new android.view.animation.OvershootInterpolator(1.2f)
|
|
1781
|
+
);
|
|
1782
|
+
|
|
1783
|
+
// Start the animation
|
|
1784
|
+
focusIndicatorView.startAnimation(scaleAnimation);
|
|
1785
|
+
|
|
1786
|
+
// Schedule fade out and removal with smoother timing
|
|
1787
|
+
focusIndicatorView.postDelayed(
|
|
1788
|
+
new Runnable() {
|
|
1789
|
+
@Override
|
|
1790
|
+
public void run() {
|
|
1791
|
+
if (focusIndicatorView != null) {
|
|
1792
|
+
// Smooth fade to semi-transparent
|
|
1793
|
+
AlphaAnimation fadeToTransparent = new AlphaAnimation(1f, 0.4f);
|
|
1794
|
+
fadeToTransparent.setDuration(400);
|
|
1795
|
+
fadeToTransparent.setInterpolator(
|
|
1796
|
+
new android.view.animation.AccelerateInterpolator()
|
|
1797
|
+
);
|
|
703
1798
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
1799
|
+
fadeToTransparent.setAnimationListener(
|
|
1800
|
+
new Animation.AnimationListener() {
|
|
1801
|
+
@Override
|
|
1802
|
+
public void onAnimationStart(Animation animation) {
|
|
1803
|
+
Log.d(TAG, "showFocusIndicator: Fade to transparent started");
|
|
1804
|
+
}
|
|
708
1805
|
|
|
709
|
-
|
|
710
|
-
|
|
1806
|
+
@Override
|
|
1807
|
+
public void onAnimationEnd(Animation animation) {
|
|
1808
|
+
Log.d(
|
|
1809
|
+
TAG,
|
|
1810
|
+
"showFocusIndicator: Fade to transparent ended, starting final fade out"
|
|
1811
|
+
);
|
|
1812
|
+
// Final smooth fade out and scale down
|
|
1813
|
+
if (focusIndicatorView != null) {
|
|
1814
|
+
AnimationSet finalAnimation = new AnimationSet(false);
|
|
1815
|
+
|
|
1816
|
+
AlphaAnimation finalFadeOut = new AlphaAnimation(0.4f, 0f);
|
|
1817
|
+
finalFadeOut.setDuration(500);
|
|
1818
|
+
finalFadeOut.setStartOffset(300);
|
|
1819
|
+
finalFadeOut.setInterpolator(
|
|
1820
|
+
new android.view.animation.AccelerateInterpolator()
|
|
1821
|
+
);
|
|
1822
|
+
|
|
1823
|
+
ScaleAnimation finalScaleDown = new ScaleAnimation(
|
|
1824
|
+
1.0f,
|
|
1825
|
+
0.9f,
|
|
1826
|
+
1.0f,
|
|
1827
|
+
0.9f,
|
|
1828
|
+
Animation.RELATIVE_TO_SELF,
|
|
1829
|
+
0.5f,
|
|
1830
|
+
Animation.RELATIVE_TO_SELF,
|
|
1831
|
+
0.5f
|
|
1832
|
+
);
|
|
1833
|
+
finalScaleDown.setDuration(500);
|
|
1834
|
+
finalScaleDown.setStartOffset(300);
|
|
1835
|
+
finalScaleDown.setInterpolator(
|
|
1836
|
+
new android.view.animation.AccelerateInterpolator()
|
|
1837
|
+
);
|
|
1838
|
+
|
|
1839
|
+
finalAnimation.addAnimation(finalFadeOut);
|
|
1840
|
+
finalAnimation.addAnimation(finalScaleDown);
|
|
1841
|
+
|
|
1842
|
+
finalAnimation.setAnimationListener(
|
|
1843
|
+
new Animation.AnimationListener() {
|
|
1844
|
+
@Override
|
|
1845
|
+
public void onAnimationStart(Animation animation) {
|
|
1846
|
+
Log.d(
|
|
1847
|
+
TAG,
|
|
1848
|
+
"showFocusIndicator: Final animation started"
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
711
1851
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1852
|
+
@Override
|
|
1853
|
+
public void onAnimationEnd(Animation animation) {
|
|
1854
|
+
Log.d(
|
|
1855
|
+
TAG,
|
|
1856
|
+
"showFocusIndicator: Final animation ended, removing indicator"
|
|
1857
|
+
);
|
|
1858
|
+
// Remove the focus indicator
|
|
1859
|
+
if (
|
|
1860
|
+
focusIndicatorView != null &&
|
|
1861
|
+
previewContainer != null
|
|
1862
|
+
) {
|
|
1863
|
+
previewContainer.removeView(focusIndicatorView);
|
|
1864
|
+
focusIndicatorView = null;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
715
1867
|
|
|
716
|
-
|
|
1868
|
+
@Override
|
|
1869
|
+
public void onAnimationRepeat(Animation animation) {}
|
|
1870
|
+
}
|
|
1871
|
+
);
|
|
717
1872
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1873
|
+
focusIndicatorView.startAnimation(finalAnimation);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
@Override
|
|
1878
|
+
public void onAnimationRepeat(Animation animation) {}
|
|
1879
|
+
}
|
|
1880
|
+
);
|
|
1881
|
+
|
|
1882
|
+
focusIndicatorView.startAnimation(fadeToTransparent);
|
|
1883
|
+
}
|
|
722
1884
|
}
|
|
1885
|
+
},
|
|
1886
|
+
800
|
|
1887
|
+
); // Optimal timing for smooth focus feedback
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
public static List<Size> getSupportedPictureSizes(String facing) {
|
|
1891
|
+
List<Size> sizes = new ArrayList<>();
|
|
1892
|
+
try {
|
|
1893
|
+
CameraSelector.Builder builder = new CameraSelector.Builder();
|
|
1894
|
+
if ("front".equals(facing)) {
|
|
1895
|
+
builder.requireLensFacing(CameraSelector.LENS_FACING_FRONT);
|
|
1896
|
+
} else {
|
|
1897
|
+
builder.requireLensFacing(CameraSelector.LENS_FACING_BACK);
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// This part is complex because we need characteristics, which are not directly on CameraInfo.
|
|
1901
|
+
// For now, returning a static list of common sizes.
|
|
1902
|
+
// A more advanced implementation would use Camera2interop to get StreamConfigurationMap.
|
|
1903
|
+
sizes.add(new Size(4032, 3024));
|
|
1904
|
+
sizes.add(new Size(1920, 1080));
|
|
1905
|
+
sizes.add(new Size(1280, 720));
|
|
1906
|
+
sizes.add(new Size(640, 480));
|
|
1907
|
+
} catch (Exception e) {
|
|
1908
|
+
Log.e(TAG, "Error getting supported picture sizes", e);
|
|
723
1909
|
}
|
|
1910
|
+
return sizes;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
private void setZoomInternal(float zoomRatio) {
|
|
1914
|
+
if (camera != null) {
|
|
1915
|
+
try {
|
|
1916
|
+
float minZoom = Objects.requireNonNull(
|
|
1917
|
+
camera.getCameraInfo().getZoomState().getValue()
|
|
1918
|
+
).getMinZoomRatio();
|
|
1919
|
+
float maxZoom = camera
|
|
1920
|
+
.getCameraInfo()
|
|
1921
|
+
.getZoomState()
|
|
1922
|
+
.getValue()
|
|
1923
|
+
.getMaxZoomRatio();
|
|
1924
|
+
float currentZoom = camera
|
|
1925
|
+
.getCameraInfo()
|
|
1926
|
+
.getZoomState()
|
|
1927
|
+
.getValue()
|
|
1928
|
+
.getZoomRatio();
|
|
1929
|
+
|
|
1930
|
+
Log.d(
|
|
1931
|
+
TAG,
|
|
1932
|
+
"setZoomInternal: Current camera range: " +
|
|
1933
|
+
minZoom +
|
|
1934
|
+
"-" +
|
|
1935
|
+
maxZoom +
|
|
1936
|
+
", current: " +
|
|
1937
|
+
currentZoom
|
|
1938
|
+
);
|
|
1939
|
+
Log.d(TAG, "setZoomInternal: Requesting zoom: " + zoomRatio);
|
|
724
1940
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1941
|
+
// Try to set zoom directly - let CameraX handle lens switching
|
|
1942
|
+
ListenableFuture<Void> zoomFuture = camera
|
|
1943
|
+
.getCameraControl()
|
|
1944
|
+
.setZoomRatio(zoomRatio);
|
|
729
1945
|
|
|
730
|
-
|
|
1946
|
+
zoomFuture.addListener(
|
|
1947
|
+
() -> {
|
|
1948
|
+
try {
|
|
1949
|
+
zoomFuture.get(); // Check if zoom was successful
|
|
1950
|
+
float newZoom = Objects.requireNonNull(
|
|
1951
|
+
camera.getCameraInfo().getZoomState().getValue()
|
|
1952
|
+
).getZoomRatio();
|
|
1953
|
+
Log.d(
|
|
1954
|
+
TAG,
|
|
1955
|
+
"setZoomInternal: Zoom set successfully to " +
|
|
1956
|
+
newZoom +
|
|
1957
|
+
" (requested: " +
|
|
1958
|
+
zoomRatio +
|
|
1959
|
+
")"
|
|
1960
|
+
);
|
|
1961
|
+
|
|
1962
|
+
// Check if CameraX switched cameras
|
|
1963
|
+
String newCameraId = getCameraId(camera.getCameraInfo());
|
|
1964
|
+
if (!newCameraId.equals(currentDeviceId)) {
|
|
1965
|
+
currentDeviceId = newCameraId;
|
|
1966
|
+
Log.d(
|
|
1967
|
+
TAG,
|
|
1968
|
+
"setZoomInternal: CameraX switched to camera: " + newCameraId
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
} catch (Exception e) {
|
|
1972
|
+
Log.w(
|
|
1973
|
+
TAG,
|
|
1974
|
+
"setZoomInternal: Zoom operation failed: " + e.getMessage()
|
|
1975
|
+
);
|
|
1976
|
+
// Fallback: clamp to current camera's range
|
|
1977
|
+
float clampedZoom = Math.max(
|
|
1978
|
+
minZoom,
|
|
1979
|
+
Math.min(zoomRatio, maxZoom)
|
|
1980
|
+
);
|
|
1981
|
+
camera.getCameraControl().setZoomRatio(clampedZoom);
|
|
1982
|
+
Log.d(
|
|
1983
|
+
TAG,
|
|
1984
|
+
"setZoomInternal: Fallback - clamped zoom to " + clampedZoom
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
},
|
|
1988
|
+
mainExecutor
|
|
1989
|
+
);
|
|
1990
|
+
} catch (Exception e) {
|
|
1991
|
+
Log.e(TAG, "setZoomInternal: Error setting zoom", e);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
public static List<String> getSupportedFlashModesStatic() {
|
|
1997
|
+
try {
|
|
1998
|
+
// For static method, we can return common flash modes
|
|
1999
|
+
// Most modern cameras support these modes
|
|
2000
|
+
return Arrays.asList("off", "on", "auto");
|
|
2001
|
+
} catch (Exception e) {
|
|
2002
|
+
Log.e(TAG, "getSupportedFlashModesStatic: Error getting flash modes", e);
|
|
2003
|
+
return Collections.singletonList("off");
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
731
2006
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
2007
|
+
public List<String> getSupportedFlashModes() {
|
|
2008
|
+
if (camera == null) {
|
|
2009
|
+
return getSupportedFlashModesStatic();
|
|
2010
|
+
}
|
|
735
2011
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
2012
|
+
try {
|
|
2013
|
+
boolean hasFlash = camera.getCameraInfo().hasFlashUnit();
|
|
2014
|
+
if (hasFlash) {
|
|
2015
|
+
return Arrays.asList("off", "on", "auto");
|
|
2016
|
+
} else {
|
|
2017
|
+
return Collections.singletonList("off");
|
|
2018
|
+
}
|
|
2019
|
+
} catch (Exception e) {
|
|
2020
|
+
Log.e(TAG, "getSupportedFlashModes: Error getting flash modes", e);
|
|
2021
|
+
return Collections.singletonList("off");
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
public String getFlashMode() {
|
|
2026
|
+
switch (currentFlashMode) {
|
|
2027
|
+
case ImageCapture.FLASH_MODE_ON:
|
|
2028
|
+
return "on";
|
|
2029
|
+
case ImageCapture.FLASH_MODE_AUTO:
|
|
2030
|
+
return "auto";
|
|
2031
|
+
default:
|
|
2032
|
+
return "off";
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
public void setFlashMode(String mode) {
|
|
2037
|
+
int flashMode;
|
|
2038
|
+
switch (mode) {
|
|
2039
|
+
case "on":
|
|
2040
|
+
flashMode = ImageCapture.FLASH_MODE_ON;
|
|
2041
|
+
break;
|
|
2042
|
+
case "auto":
|
|
2043
|
+
flashMode = ImageCapture.FLASH_MODE_AUTO;
|
|
2044
|
+
break;
|
|
2045
|
+
default:
|
|
2046
|
+
flashMode = ImageCapture.FLASH_MODE_OFF;
|
|
2047
|
+
break;
|
|
2048
|
+
}
|
|
750
2049
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
2050
|
+
currentFlashMode = flashMode;
|
|
2051
|
+
|
|
2052
|
+
if (imageCapture != null) {
|
|
2053
|
+
imageCapture.setFlashMode(flashMode);
|
|
2054
|
+
}
|
|
2055
|
+
if (sampleImageCapture != null) {
|
|
2056
|
+
sampleImageCapture.setFlashMode(flashMode);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
public String getCurrentDeviceId() {
|
|
2061
|
+
return currentDeviceId != null ? currentDeviceId : "unknown";
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
2065
|
+
public void switchToDevice(String deviceId) {
|
|
2066
|
+
Log.d(TAG, "switchToDevice: Attempting to switch to device " + deviceId);
|
|
2067
|
+
|
|
2068
|
+
mainExecutor.execute(() -> {
|
|
2069
|
+
try {
|
|
2070
|
+
// Standard physical device selection logic...
|
|
2071
|
+
List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
|
|
2072
|
+
CameraInfo targetCameraInfo = null;
|
|
2073
|
+
for (CameraInfo cameraInfo : cameraInfos) {
|
|
2074
|
+
if (
|
|
2075
|
+
deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())
|
|
2076
|
+
) {
|
|
2077
|
+
targetCameraInfo = cameraInfo;
|
|
2078
|
+
break;
|
|
2079
|
+
}
|
|
754
2080
|
}
|
|
2081
|
+
|
|
2082
|
+
if (targetCameraInfo != null) {
|
|
2083
|
+
Log.d(
|
|
2084
|
+
TAG,
|
|
2085
|
+
"switchToDevice: Found matching CameraInfo for deviceId: " +
|
|
2086
|
+
deviceId
|
|
2087
|
+
);
|
|
2088
|
+
final CameraInfo finalTarget = targetCameraInfo;
|
|
2089
|
+
|
|
2090
|
+
// This filter will receive a list of all cameras and must return the one we want.
|
|
2091
|
+
|
|
2092
|
+
currentCameraSelector = new CameraSelector.Builder()
|
|
2093
|
+
.addCameraFilter(cameras -> {
|
|
2094
|
+
// This filter will receive a list of all cameras and must return the one we want.
|
|
2095
|
+
return Collections.singletonList(finalTarget);
|
|
2096
|
+
})
|
|
2097
|
+
.build();
|
|
2098
|
+
currentDeviceId = deviceId;
|
|
2099
|
+
bindCameraUseCases(); // Rebind with the new, highly specific selector
|
|
2100
|
+
} else {
|
|
2101
|
+
Log.e(
|
|
2102
|
+
TAG,
|
|
2103
|
+
"switchToDevice: Could not find any CameraInfo matching deviceId: " +
|
|
2104
|
+
deviceId
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
} catch (Exception e) {
|
|
2108
|
+
Log.e(TAG, "switchToDevice: Error switching camera", e);
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
public void flipCamera() {
|
|
2114
|
+
Log.d(TAG, "flipCamera: Flipping camera");
|
|
2115
|
+
|
|
2116
|
+
// Determine current position based on session config and flip it
|
|
2117
|
+
String currentPosition = sessionConfig.getPosition();
|
|
2118
|
+
String newPosition = "front".equals(currentPosition) ? "rear" : "front";
|
|
2119
|
+
|
|
2120
|
+
Log.d(
|
|
2121
|
+
TAG,
|
|
2122
|
+
"flipCamera: Switching from " + currentPosition + " to " + newPosition
|
|
2123
|
+
);
|
|
2124
|
+
|
|
2125
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
2126
|
+
null, // deviceId - clear device ID to force position-based selection
|
|
2127
|
+
newPosition, // position
|
|
2128
|
+
sessionConfig.getX(), // x
|
|
2129
|
+
sessionConfig.getY(), // y
|
|
2130
|
+
sessionConfig.getWidth(), // width
|
|
2131
|
+
sessionConfig.getHeight(), // height
|
|
2132
|
+
sessionConfig.getPaddingBottom(), // paddingBottom
|
|
2133
|
+
sessionConfig.isToBack(), // toBack
|
|
2134
|
+
sessionConfig.isStoreToFile(), // storeToFile
|
|
2135
|
+
sessionConfig.isEnableOpacity(), // enableOpacity
|
|
2136
|
+
sessionConfig.isEnableZoom(), // enableZoom
|
|
2137
|
+
sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
|
|
2138
|
+
sessionConfig.isDisableAudio(), // disableAudio
|
|
2139
|
+
sessionConfig.getZoomFactor(), // zoomFactor
|
|
2140
|
+
sessionConfig.getAspectRatio(), // aspectRatio
|
|
2141
|
+
sessionConfig.getGridMode() // gridMode
|
|
2142
|
+
);
|
|
2143
|
+
|
|
2144
|
+
// Clear current device ID to force position-based selection
|
|
2145
|
+
currentDeviceId = null;
|
|
2146
|
+
|
|
2147
|
+
// Camera operations must run on main thread
|
|
2148
|
+
cameraExecutor.execute(() -> {
|
|
2149
|
+
currentCameraSelector = buildCameraSelector();
|
|
2150
|
+
bindCameraUseCases();
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
public void setOpacity(float opacity) {
|
|
2155
|
+
if (previewView != null) {
|
|
2156
|
+
previewView.setAlpha(opacity);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
private void updateLayoutParams() {
|
|
2161
|
+
if (sessionConfig == null) return;
|
|
2162
|
+
|
|
2163
|
+
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
|
|
2164
|
+
sessionConfig.getWidth(),
|
|
2165
|
+
sessionConfig.getHeight()
|
|
2166
|
+
);
|
|
2167
|
+
layoutParams.leftMargin = sessionConfig.getX();
|
|
2168
|
+
layoutParams.topMargin = sessionConfig.getY();
|
|
2169
|
+
|
|
2170
|
+
if (sessionConfig.getAspectRatio() != null) {
|
|
2171
|
+
String[] ratios = sessionConfig.getAspectRatio().split(":");
|
|
2172
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
2173
|
+
float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
|
|
2174
|
+
if (sessionConfig.getWidth() > 0) {
|
|
2175
|
+
layoutParams.height = (int) (sessionConfig.getWidth() / ratio);
|
|
2176
|
+
} else if (sessionConfig.getHeight() > 0) {
|
|
2177
|
+
layoutParams.width = (int) (sessionConfig.getHeight() * ratio);
|
|
2178
|
+
}
|
|
755
2179
|
}
|
|
756
2180
|
|
|
757
|
-
|
|
758
|
-
List<Size> sizes = new ArrayList<>();
|
|
759
|
-
try {
|
|
760
|
-
CameraSelector.Builder builder = new CameraSelector.Builder();
|
|
761
|
-
if ("front".equals(facing)) {
|
|
762
|
-
builder.requireLensFacing(CameraSelector.LENS_FACING_FRONT);
|
|
763
|
-
} else {
|
|
764
|
-
builder.requireLensFacing(CameraSelector.LENS_FACING_BACK);
|
|
765
|
-
}
|
|
2181
|
+
previewView.setLayoutParams(layoutParams);
|
|
766
2182
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
2183
|
+
if (listener != null) {
|
|
2184
|
+
listener.onCameraStarted(
|
|
2185
|
+
sessionConfig.getWidth(),
|
|
2186
|
+
sessionConfig.getHeight(),
|
|
2187
|
+
sessionConfig.getX(),
|
|
2188
|
+
sessionConfig.getY()
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
774
2192
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
return sizes;
|
|
2193
|
+
public String getAspectRatio() {
|
|
2194
|
+
if (sessionConfig != null) {
|
|
2195
|
+
return sessionConfig.getAspectRatio();
|
|
779
2196
|
}
|
|
2197
|
+
return "4:3";
|
|
2198
|
+
}
|
|
780
2199
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
} catch (Exception e) {
|
|
807
|
-
Log.w(TAG, "setZoomInternal: Zoom operation failed: " + e.getMessage());
|
|
808
|
-
// Fallback: clamp to current camera's range
|
|
809
|
-
float clampedZoom = Math.max(minZoom, Math.min(zoomRatio, maxZoom));
|
|
810
|
-
camera.getCameraControl().setZoomRatio(clampedZoom);
|
|
811
|
-
Log.d(TAG, "setZoomInternal: Fallback - clamped zoom to " + clampedZoom);
|
|
812
|
-
}
|
|
813
|
-
}, mainExecutor);
|
|
2200
|
+
public String getGridMode() {
|
|
2201
|
+
if (sessionConfig != null) {
|
|
2202
|
+
return sessionConfig.getGridMode();
|
|
2203
|
+
}
|
|
2204
|
+
return "none";
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
public void setAspectRatio(String aspectRatio) {
|
|
2208
|
+
setAspectRatio(aspectRatio, null, null);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
public void setAspectRatio(String aspectRatio, Float x, Float y) {
|
|
2212
|
+
setAspectRatio(aspectRatio, x, y, null);
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
public void setAspectRatio(
|
|
2216
|
+
String aspectRatio,
|
|
2217
|
+
Float x,
|
|
2218
|
+
Float y,
|
|
2219
|
+
Runnable callback
|
|
2220
|
+
) {
|
|
2221
|
+
if (sessionConfig == null) {
|
|
2222
|
+
if (callback != null) callback.run();
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
814
2225
|
|
|
815
|
-
|
|
816
|
-
|
|
2226
|
+
String currentAspectRatio = sessionConfig.getAspectRatio();
|
|
2227
|
+
|
|
2228
|
+
// Don't restart camera if aspect ratio hasn't changed and no position specified
|
|
2229
|
+
if (
|
|
2230
|
+
aspectRatio != null &&
|
|
2231
|
+
aspectRatio.equals(currentAspectRatio) &&
|
|
2232
|
+
x == null &&
|
|
2233
|
+
y == null
|
|
2234
|
+
) {
|
|
2235
|
+
Log.d(
|
|
2236
|
+
TAG,
|
|
2237
|
+
"setAspectRatio: Aspect ratio " +
|
|
2238
|
+
aspectRatio +
|
|
2239
|
+
" is already set and no position specified, skipping"
|
|
2240
|
+
);
|
|
2241
|
+
if (callback != null) callback.run();
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
String currentGridMode = sessionConfig.getGridMode();
|
|
2246
|
+
Log.d(
|
|
2247
|
+
TAG,
|
|
2248
|
+
"setAspectRatio: Changing from " +
|
|
2249
|
+
currentAspectRatio +
|
|
2250
|
+
" to " +
|
|
2251
|
+
aspectRatio +
|
|
2252
|
+
(x != null && y != null
|
|
2253
|
+
? " at position (" + x + ", " + y + ")"
|
|
2254
|
+
: " with auto-centering") +
|
|
2255
|
+
", preserving grid mode: " +
|
|
2256
|
+
currentGridMode
|
|
2257
|
+
);
|
|
2258
|
+
|
|
2259
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
2260
|
+
sessionConfig.getDeviceId(),
|
|
2261
|
+
sessionConfig.getPosition(),
|
|
2262
|
+
sessionConfig.getX(),
|
|
2263
|
+
sessionConfig.getY(),
|
|
2264
|
+
sessionConfig.getWidth(),
|
|
2265
|
+
sessionConfig.getHeight(),
|
|
2266
|
+
sessionConfig.getPaddingBottom(),
|
|
2267
|
+
sessionConfig.getToBack(),
|
|
2268
|
+
sessionConfig.getStoreToFile(),
|
|
2269
|
+
sessionConfig.getEnableOpacity(),
|
|
2270
|
+
sessionConfig.getEnableZoom(),
|
|
2271
|
+
sessionConfig.getDisableExifHeaderStripping(),
|
|
2272
|
+
sessionConfig.getDisableAudio(),
|
|
2273
|
+
sessionConfig.getZoomFactor(),
|
|
2274
|
+
aspectRatio,
|
|
2275
|
+
currentGridMode
|
|
2276
|
+
);
|
|
2277
|
+
|
|
2278
|
+
// Update layout and rebind camera with new aspect ratio
|
|
2279
|
+
if (isRunning && previewContainer != null) {
|
|
2280
|
+
mainExecutor.execute(() -> {
|
|
2281
|
+
// First update the UI layout
|
|
2282
|
+
updatePreviewLayoutForAspectRatio(aspectRatio, x, y);
|
|
2283
|
+
|
|
2284
|
+
// Then rebind the camera with new aspect ratio configuration
|
|
2285
|
+
Log.d(
|
|
2286
|
+
TAG,
|
|
2287
|
+
"setAspectRatio: Rebinding camera with new aspect ratio: " +
|
|
2288
|
+
aspectRatio
|
|
2289
|
+
);
|
|
2290
|
+
bindCameraUseCases();
|
|
2291
|
+
|
|
2292
|
+
// Preserve grid mode and wait for completion
|
|
2293
|
+
if (gridOverlayView != null) {
|
|
2294
|
+
gridOverlayView.post(() -> {
|
|
2295
|
+
Log.d(
|
|
2296
|
+
TAG,
|
|
2297
|
+
"setAspectRatio: Re-applying grid mode: " + currentGridMode
|
|
2298
|
+
);
|
|
2299
|
+
gridOverlayView.setGridMode(currentGridMode);
|
|
2300
|
+
|
|
2301
|
+
// Wait one more frame for grid to be applied, then call callback
|
|
2302
|
+
if (callback != null) {
|
|
2303
|
+
gridOverlayView.post(callback);
|
|
817
2304
|
}
|
|
2305
|
+
});
|
|
2306
|
+
} else {
|
|
2307
|
+
// No grid overlay, wait one frame for layout completion then call callback
|
|
2308
|
+
if (callback != null) {
|
|
2309
|
+
previewContainer.post(callback);
|
|
2310
|
+
}
|
|
818
2311
|
}
|
|
2312
|
+
});
|
|
2313
|
+
} else {
|
|
2314
|
+
if (callback != null) callback.run();
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
public void setGridMode(String gridMode) {
|
|
2319
|
+
if (sessionConfig != null) {
|
|
2320
|
+
Log.d(TAG, "setGridMode: Changing grid mode to: " + gridMode);
|
|
2321
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
2322
|
+
sessionConfig.getDeviceId(),
|
|
2323
|
+
sessionConfig.getPosition(),
|
|
2324
|
+
sessionConfig.getX(),
|
|
2325
|
+
sessionConfig.getY(),
|
|
2326
|
+
sessionConfig.getWidth(),
|
|
2327
|
+
sessionConfig.getHeight(),
|
|
2328
|
+
sessionConfig.getPaddingBottom(),
|
|
2329
|
+
sessionConfig.getToBack(),
|
|
2330
|
+
sessionConfig.getStoreToFile(),
|
|
2331
|
+
sessionConfig.getEnableOpacity(),
|
|
2332
|
+
sessionConfig.getEnableZoom(),
|
|
2333
|
+
sessionConfig.getDisableExifHeaderStripping(),
|
|
2334
|
+
sessionConfig.getDisableAudio(),
|
|
2335
|
+
sessionConfig.getZoomFactor(),
|
|
2336
|
+
sessionConfig.getAspectRatio(),
|
|
2337
|
+
gridMode
|
|
2338
|
+
);
|
|
2339
|
+
|
|
2340
|
+
// Update the grid overlay immediately
|
|
2341
|
+
if (gridOverlayView != null) {
|
|
2342
|
+
gridOverlayView.post(() -> {
|
|
2343
|
+
Log.d(TAG, "setGridMode: Applying grid mode to overlay: " + gridMode);
|
|
2344
|
+
gridOverlayView.setGridMode(gridMode);
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
public int getPreviewX() {
|
|
2351
|
+
if (previewContainer == null) return 0;
|
|
2352
|
+
|
|
2353
|
+
// Get the container position
|
|
2354
|
+
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
2355
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
2356
|
+
int containerX = ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin;
|
|
2357
|
+
|
|
2358
|
+
// Get the actual camera bounds within the container
|
|
2359
|
+
Rect cameraBounds = getActualCameraBounds();
|
|
2360
|
+
int actualX = containerX + cameraBounds.left;
|
|
2361
|
+
|
|
2362
|
+
Log.d(
|
|
2363
|
+
TAG,
|
|
2364
|
+
"getPreviewX: containerX=" +
|
|
2365
|
+
containerX +
|
|
2366
|
+
", cameraBounds.left=" +
|
|
2367
|
+
cameraBounds.left +
|
|
2368
|
+
", actualX=" +
|
|
2369
|
+
actualX
|
|
2370
|
+
);
|
|
2371
|
+
|
|
2372
|
+
return actualX;
|
|
2373
|
+
}
|
|
2374
|
+
return previewContainer.getLeft();
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
public int getPreviewY() {
|
|
2378
|
+
if (previewContainer == null) return 0;
|
|
2379
|
+
|
|
2380
|
+
// Get the container position
|
|
2381
|
+
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
2382
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
2383
|
+
int containerY = ((ViewGroup.MarginLayoutParams) layoutParams).topMargin;
|
|
2384
|
+
|
|
2385
|
+
// Get the actual camera bounds within the container
|
|
2386
|
+
Rect cameraBounds = getActualCameraBounds();
|
|
2387
|
+
int actualY = containerY + cameraBounds.top;
|
|
2388
|
+
|
|
2389
|
+
Log.d(
|
|
2390
|
+
TAG,
|
|
2391
|
+
"getPreviewY: containerY=" +
|
|
2392
|
+
containerY +
|
|
2393
|
+
", cameraBounds.top=" +
|
|
2394
|
+
cameraBounds.top +
|
|
2395
|
+
", actualY=" +
|
|
2396
|
+
actualY
|
|
2397
|
+
);
|
|
2398
|
+
|
|
2399
|
+
return actualY;
|
|
819
2400
|
}
|
|
2401
|
+
return previewContainer.getTop();
|
|
2402
|
+
}
|
|
820
2403
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
2404
|
+
// Get the actual camera content bounds within the PreviewView
|
|
2405
|
+
private Rect getActualCameraBounds() {
|
|
2406
|
+
if (previewView == null || previewContainer == null) {
|
|
2407
|
+
return new Rect(0, 0, 0, 0);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// Get the container bounds
|
|
2411
|
+
int containerWidth = previewContainer.getWidth();
|
|
2412
|
+
int containerHeight = previewContainer.getHeight();
|
|
2413
|
+
|
|
2414
|
+
// Get the preview transformation info to understand how the camera is scaled/positioned
|
|
2415
|
+
// For FIT_CENTER, the camera content is scaled to fit within the container
|
|
2416
|
+
// This might create letterboxing (black bars) on top/bottom or left/right
|
|
2417
|
+
|
|
2418
|
+
// Get the actual preview resolution
|
|
2419
|
+
if (currentPreviewResolution == null) {
|
|
2420
|
+
// If we don't have the resolution yet, assume the container is filled
|
|
2421
|
+
return new Rect(0, 0, containerWidth, containerHeight);
|
|
830
2422
|
}
|
|
831
2423
|
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
2424
|
+
// The preview is rotated 90 degrees for portrait mode
|
|
2425
|
+
// So we swap the dimensions
|
|
2426
|
+
int cameraWidth = currentPreviewResolution.getHeight();
|
|
2427
|
+
int cameraHeight = currentPreviewResolution.getWidth();
|
|
2428
|
+
|
|
2429
|
+
// Calculate the scaling factor to fit the camera in the container
|
|
2430
|
+
float widthScale = (float) containerWidth / cameraWidth;
|
|
2431
|
+
float heightScale = (float) containerHeight / cameraHeight;
|
|
2432
|
+
float scale = Math.min(widthScale, heightScale); // FIT_CENTER uses min scale
|
|
2433
|
+
|
|
2434
|
+
// Calculate the actual size of the camera content after scaling
|
|
2435
|
+
int scaledWidth = Math.round(cameraWidth * scale);
|
|
2436
|
+
int scaledHeight = Math.round(cameraHeight * scale);
|
|
2437
|
+
|
|
2438
|
+
// Calculate the offset to center the content
|
|
2439
|
+
int offsetX = (containerWidth - scaledWidth) / 2;
|
|
2440
|
+
int offsetY = (containerHeight - scaledHeight) / 2;
|
|
2441
|
+
|
|
2442
|
+
Log.d(
|
|
2443
|
+
TAG,
|
|
2444
|
+
"getActualCameraBounds: container=" +
|
|
2445
|
+
containerWidth +
|
|
2446
|
+
"x" +
|
|
2447
|
+
containerHeight +
|
|
2448
|
+
", camera=" +
|
|
2449
|
+
cameraWidth +
|
|
2450
|
+
"x" +
|
|
2451
|
+
cameraHeight +
|
|
2452
|
+
", scale=" +
|
|
2453
|
+
scale +
|
|
2454
|
+
", scaled=" +
|
|
2455
|
+
scaledWidth +
|
|
2456
|
+
"x" +
|
|
2457
|
+
scaledHeight +
|
|
2458
|
+
", offset=(" +
|
|
2459
|
+
offsetX +
|
|
2460
|
+
"," +
|
|
2461
|
+
offsetY +
|
|
2462
|
+
")"
|
|
2463
|
+
);
|
|
2464
|
+
|
|
2465
|
+
// Return the bounds relative to the container
|
|
2466
|
+
return new Rect(
|
|
2467
|
+
offsetX,
|
|
2468
|
+
offsetY,
|
|
2469
|
+
offsetX + scaledWidth,
|
|
2470
|
+
offsetY + scaledHeight
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
public int getPreviewWidth() {
|
|
2475
|
+
if (previewContainer == null) return 0;
|
|
2476
|
+
Rect bounds = getActualCameraBounds();
|
|
2477
|
+
return bounds.width();
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
public int getPreviewHeight() {
|
|
2481
|
+
if (previewContainer == null) return 0;
|
|
2482
|
+
Rect bounds = getActualCameraBounds();
|
|
2483
|
+
return bounds.height();
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
public void setPreviewSize(int x, int y, int width, int height) {
|
|
2487
|
+
setPreviewSize(x, y, width, height, null);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
public void setPreviewSize(
|
|
2491
|
+
int x,
|
|
2492
|
+
int y,
|
|
2493
|
+
int width,
|
|
2494
|
+
int height,
|
|
2495
|
+
Runnable callback
|
|
2496
|
+
) {
|
|
2497
|
+
if (previewContainer == null) {
|
|
2498
|
+
if (callback != null) callback.run();
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// Ensure this runs on the main UI thread
|
|
2503
|
+
mainExecutor.execute(() -> {
|
|
2504
|
+
ViewGroup.LayoutParams layoutParams = previewContainer.getLayoutParams();
|
|
2505
|
+
if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
2506
|
+
ViewGroup.MarginLayoutParams params =
|
|
2507
|
+
(ViewGroup.MarginLayoutParams) layoutParams;
|
|
2508
|
+
|
|
2509
|
+
// Only add insets for positioning coordinates, not for full-screen sizes
|
|
2510
|
+
int webViewTopInset = getWebViewTopInset();
|
|
2511
|
+
int webViewLeftInset = getWebViewLeftInset();
|
|
2512
|
+
|
|
2513
|
+
// Handle positioning - preserve current values if new values are not specified (negative)
|
|
2514
|
+
if (x >= 0) {
|
|
2515
|
+
// Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
|
|
2516
|
+
if (x == 0 && y == 0) {
|
|
2517
|
+
params.leftMargin = x;
|
|
2518
|
+
Log.d(
|
|
2519
|
+
TAG,
|
|
2520
|
+
"setPreviewSize: Full-screen mode - keeping x=0 without insets"
|
|
2521
|
+
);
|
|
2522
|
+
} else {
|
|
2523
|
+
params.leftMargin = x + webViewLeftInset;
|
|
2524
|
+
Log.d(
|
|
2525
|
+
TAG,
|
|
2526
|
+
"setPreviewSize: Positioned mode - x=" +
|
|
2527
|
+
x +
|
|
2528
|
+
" + inset=" +
|
|
2529
|
+
webViewLeftInset +
|
|
2530
|
+
" = " +
|
|
2531
|
+
(x + webViewLeftInset)
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
835
2534
|
}
|
|
2535
|
+
if (y >= 0) {
|
|
2536
|
+
// Don't add insets if this looks like a calculated full-screen coordinate (x=0, y=0)
|
|
2537
|
+
if (x == 0 && y == 0) {
|
|
2538
|
+
params.topMargin = y;
|
|
2539
|
+
Log.d(
|
|
2540
|
+
TAG,
|
|
2541
|
+
"setPreviewSize: Full-screen mode - keeping y=0 without insets"
|
|
2542
|
+
);
|
|
2543
|
+
} else {
|
|
2544
|
+
params.topMargin = y + webViewTopInset;
|
|
2545
|
+
Log.d(
|
|
2546
|
+
TAG,
|
|
2547
|
+
"setPreviewSize: Positioned mode - y=" +
|
|
2548
|
+
y +
|
|
2549
|
+
" + inset=" +
|
|
2550
|
+
webViewTopInset +
|
|
2551
|
+
" = " +
|
|
2552
|
+
(y + webViewTopInset)
|
|
2553
|
+
);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
if (width > 0) params.width = width;
|
|
2557
|
+
if (height > 0) params.height = height;
|
|
2558
|
+
|
|
2559
|
+
previewContainer.setLayoutParams(params);
|
|
2560
|
+
previewContainer.requestLayout();
|
|
2561
|
+
|
|
2562
|
+
Log.d(
|
|
2563
|
+
TAG,
|
|
2564
|
+
"setPreviewSize: Updated to " +
|
|
2565
|
+
params.width +
|
|
2566
|
+
"x" +
|
|
2567
|
+
params.height +
|
|
2568
|
+
" at (" +
|
|
2569
|
+
params.leftMargin +
|
|
2570
|
+
"," +
|
|
2571
|
+
params.topMargin +
|
|
2572
|
+
")"
|
|
2573
|
+
);
|
|
836
2574
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
2575
|
+
// Update session config to reflect actual layout
|
|
2576
|
+
if (sessionConfig != null) {
|
|
2577
|
+
String currentAspectRatio = sessionConfig.getAspectRatio();
|
|
2578
|
+
|
|
2579
|
+
// Calculate aspect ratio from actual dimensions if both width and height are provided
|
|
2580
|
+
String calculatedAspectRatio = currentAspectRatio;
|
|
2581
|
+
if (params.width > 0 && params.height > 0) {
|
|
2582
|
+
// Always use larger dimension / smaller dimension for consistent comparison
|
|
2583
|
+
float ratio =
|
|
2584
|
+
Math.max(params.width, params.height) /
|
|
2585
|
+
(float) Math.min(params.width, params.height);
|
|
2586
|
+
// Standard ratios: 16:9 ≈ 1.778, 4:3 ≈ 1.333
|
|
2587
|
+
float ratio16_9 = 16f / 9f; // 1.778
|
|
2588
|
+
float ratio4_3 = 4f / 3f; // 1.333
|
|
2589
|
+
|
|
2590
|
+
// Determine closest standard aspect ratio
|
|
2591
|
+
if (Math.abs(ratio - ratio16_9) < Math.abs(ratio - ratio4_3)) {
|
|
2592
|
+
calculatedAspectRatio = "16:9";
|
|
841
2593
|
} else {
|
|
842
|
-
|
|
2594
|
+
calculatedAspectRatio = "4:3";
|
|
843
2595
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
2596
|
+
Log.d(
|
|
2597
|
+
TAG,
|
|
2598
|
+
"setPreviewSize: Calculated aspect ratio from " +
|
|
2599
|
+
params.width +
|
|
2600
|
+
"x" +
|
|
2601
|
+
params.height +
|
|
2602
|
+
" = " +
|
|
2603
|
+
calculatedAspectRatio +
|
|
2604
|
+
" (normalized ratio=" +
|
|
2605
|
+
ratio +
|
|
2606
|
+
")"
|
|
2607
|
+
);
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
2611
|
+
sessionConfig.getDeviceId(),
|
|
2612
|
+
sessionConfig.getPosition(),
|
|
2613
|
+
params.leftMargin,
|
|
2614
|
+
params.topMargin,
|
|
2615
|
+
params.width,
|
|
2616
|
+
params.height,
|
|
2617
|
+
sessionConfig.getPaddingBottom(),
|
|
2618
|
+
sessionConfig.getToBack(),
|
|
2619
|
+
sessionConfig.getStoreToFile(),
|
|
2620
|
+
sessionConfig.getEnableOpacity(),
|
|
2621
|
+
sessionConfig.getEnableZoom(),
|
|
2622
|
+
sessionConfig.getDisableExifHeaderStripping(),
|
|
2623
|
+
sessionConfig.getDisableAudio(),
|
|
2624
|
+
sessionConfig.getZoomFactor(),
|
|
2625
|
+
calculatedAspectRatio,
|
|
2626
|
+
sessionConfig.getGridMode()
|
|
2627
|
+
);
|
|
2628
|
+
|
|
2629
|
+
// If aspect ratio changed due to size update, rebind camera
|
|
2630
|
+
if (
|
|
2631
|
+
isRunning &&
|
|
2632
|
+
!Objects.equals(currentAspectRatio, calculatedAspectRatio)
|
|
2633
|
+
) {
|
|
2634
|
+
Log.d(
|
|
2635
|
+
TAG,
|
|
2636
|
+
"setPreviewSize: Aspect ratio changed from " +
|
|
2637
|
+
currentAspectRatio +
|
|
2638
|
+
" to " +
|
|
2639
|
+
calculatedAspectRatio +
|
|
2640
|
+
", rebinding camera"
|
|
2641
|
+
);
|
|
2642
|
+
bindCameraUseCases();
|
|
849
2643
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
2644
|
+
// Wait for camera rebinding to complete, then call callback
|
|
2645
|
+
if (callback != null) {
|
|
2646
|
+
previewContainer.post(() -> {
|
|
2647
|
+
updateGridOverlayBounds();
|
|
2648
|
+
previewContainer.post(callback);
|
|
2649
|
+
});
|
|
2650
|
+
} else {
|
|
2651
|
+
previewContainer.post(() -> updateGridOverlayBounds());
|
|
2652
|
+
}
|
|
2653
|
+
} else {
|
|
2654
|
+
// No camera rebinding needed, wait for layout to complete then call callback
|
|
2655
|
+
previewContainer.post(() -> {
|
|
2656
|
+
updateGridOverlayBounds();
|
|
2657
|
+
if (callback != null) {
|
|
2658
|
+
callback.run();
|
|
2659
|
+
}
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
} else {
|
|
2663
|
+
// No sessionConfig, just wait for layout then call callback
|
|
2664
|
+
previewContainer.post(() -> {
|
|
2665
|
+
updateGridOverlayBounds();
|
|
2666
|
+
if (callback != null) {
|
|
2667
|
+
callback.run();
|
|
2668
|
+
}
|
|
2669
|
+
});
|
|
858
2670
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
2671
|
+
} else {
|
|
2672
|
+
Log.w(
|
|
2673
|
+
TAG,
|
|
2674
|
+
"setPreviewSize: Cannot set margins on layout params of type " +
|
|
2675
|
+
layoutParams.getClass().getSimpleName()
|
|
2676
|
+
);
|
|
2677
|
+
// Fallback: just set width and height if specified
|
|
2678
|
+
if (width > 0) layoutParams.width = width;
|
|
2679
|
+
if (height > 0) layoutParams.height = height;
|
|
2680
|
+
previewContainer.setLayoutParams(layoutParams);
|
|
2681
|
+
previewContainer.requestLayout();
|
|
2682
|
+
|
|
2683
|
+
// Wait for layout then call callback
|
|
2684
|
+
if (callback != null) {
|
|
2685
|
+
previewContainer.post(callback);
|
|
873
2686
|
}
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
private void updatePreviewLayoutForAspectRatio(String aspectRatio) {
|
|
2692
|
+
updatePreviewLayoutForAspectRatio(aspectRatio, null, null);
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
private void updatePreviewLayoutForAspectRatio(
|
|
2696
|
+
String aspectRatio,
|
|
2697
|
+
Float x,
|
|
2698
|
+
Float y
|
|
2699
|
+
) {
|
|
2700
|
+
if (previewContainer == null || aspectRatio == null) return;
|
|
2701
|
+
|
|
2702
|
+
// Parse aspect ratio
|
|
2703
|
+
String[] ratios = aspectRatio.split(":");
|
|
2704
|
+
if (ratios.length != 2) return;
|
|
2705
|
+
|
|
2706
|
+
try {
|
|
2707
|
+
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
2708
|
+
float ratio = Float.parseFloat(ratios[1]) / Float.parseFloat(ratios[0]);
|
|
2709
|
+
|
|
2710
|
+
// Get available space from webview dimensions
|
|
2711
|
+
int availableWidth = webView.getWidth();
|
|
2712
|
+
int availableHeight = webView.getHeight();
|
|
2713
|
+
|
|
2714
|
+
// Calculate position and size
|
|
2715
|
+
int finalX, finalY, finalWidth, finalHeight;
|
|
2716
|
+
|
|
2717
|
+
if (x != null && y != null) {
|
|
2718
|
+
// Account for WebView insets from edge-to-edge support
|
|
2719
|
+
int webViewTopInset = getWebViewTopInset();
|
|
2720
|
+
int webViewLeftInset = getWebViewLeftInset();
|
|
2721
|
+
|
|
2722
|
+
// Use provided coordinates with boundary checking, adjusted for insets
|
|
2723
|
+
finalX = Math.max(
|
|
2724
|
+
0,
|
|
2725
|
+
Math.min(x.intValue() + webViewLeftInset, availableWidth)
|
|
2726
|
+
);
|
|
2727
|
+
finalY = Math.max(
|
|
2728
|
+
0,
|
|
2729
|
+
Math.min(y.intValue() + webViewTopInset, availableHeight)
|
|
2730
|
+
);
|
|
874
2731
|
|
|
875
|
-
|
|
2732
|
+
// Calculate maximum available space from the given position
|
|
2733
|
+
int maxWidth = availableWidth - finalX;
|
|
2734
|
+
int maxHeight = availableHeight - finalY;
|
|
876
2735
|
|
|
877
|
-
|
|
878
|
-
|
|
2736
|
+
// Calculate optimal size while maintaining aspect ratio within available space
|
|
2737
|
+
finalWidth = maxWidth;
|
|
2738
|
+
finalHeight = (int) (maxWidth / ratio);
|
|
2739
|
+
|
|
2740
|
+
if (finalHeight > maxHeight) {
|
|
2741
|
+
// Height constraint is tighter, fit by height
|
|
2742
|
+
finalHeight = maxHeight;
|
|
2743
|
+
finalWidth = (int) (maxHeight * ratio);
|
|
879
2744
|
}
|
|
880
|
-
|
|
881
|
-
|
|
2745
|
+
|
|
2746
|
+
// Ensure final position stays within bounds
|
|
2747
|
+
finalX = Math.max(0, Math.min(finalX, availableWidth - finalWidth));
|
|
2748
|
+
finalY = Math.max(0, Math.min(finalY, availableHeight - finalHeight));
|
|
2749
|
+
} else {
|
|
2750
|
+
// Auto-center the view
|
|
2751
|
+
// Use full available space to match iOS behavior
|
|
2752
|
+
int maxAvailableWidth = availableWidth;
|
|
2753
|
+
int maxAvailableHeight = availableHeight;
|
|
2754
|
+
|
|
2755
|
+
// Start with width-based calculation
|
|
2756
|
+
finalWidth = maxAvailableWidth;
|
|
2757
|
+
finalHeight = (int) (finalWidth / ratio);
|
|
2758
|
+
|
|
2759
|
+
// If height exceeds available space, use height-based calculation
|
|
2760
|
+
if (finalHeight > maxAvailableHeight) {
|
|
2761
|
+
finalHeight = maxAvailableHeight;
|
|
2762
|
+
finalWidth = (int) (finalHeight * ratio);
|
|
882
2763
|
}
|
|
2764
|
+
|
|
2765
|
+
// Center the view
|
|
2766
|
+
finalX = (availableWidth - finalWidth) / 2;
|
|
2767
|
+
finalY = (availableHeight - finalHeight) / 2;
|
|
2768
|
+
|
|
2769
|
+
Log.d(
|
|
2770
|
+
TAG,
|
|
2771
|
+
"updatePreviewLayoutForAspectRatio: Auto-center mode - ratio=" +
|
|
2772
|
+
ratio +
|
|
2773
|
+
", calculated size=" +
|
|
2774
|
+
finalWidth +
|
|
2775
|
+
"x" +
|
|
2776
|
+
finalHeight +
|
|
2777
|
+
", available=" +
|
|
2778
|
+
availableWidth +
|
|
2779
|
+
"x" +
|
|
2780
|
+
availableHeight
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// Update layout params
|
|
2785
|
+
ViewGroup.LayoutParams currentParams = previewContainer.getLayoutParams();
|
|
2786
|
+
if (currentParams instanceof ViewGroup.MarginLayoutParams) {
|
|
2787
|
+
ViewGroup.MarginLayoutParams params =
|
|
2788
|
+
(ViewGroup.MarginLayoutParams) currentParams;
|
|
2789
|
+
params.width = finalWidth;
|
|
2790
|
+
params.height = finalHeight;
|
|
2791
|
+
params.leftMargin = finalX;
|
|
2792
|
+
params.topMargin = finalY;
|
|
2793
|
+
previewContainer.setLayoutParams(params);
|
|
2794
|
+
previewContainer.requestLayout();
|
|
2795
|
+
Log.d(
|
|
2796
|
+
TAG,
|
|
2797
|
+
"updatePreviewLayoutForAspectRatio: Updated to " +
|
|
2798
|
+
finalWidth +
|
|
2799
|
+
"x" +
|
|
2800
|
+
finalHeight +
|
|
2801
|
+
" at (" +
|
|
2802
|
+
finalX +
|
|
2803
|
+
"," +
|
|
2804
|
+
finalY +
|
|
2805
|
+
")"
|
|
2806
|
+
);
|
|
2807
|
+
|
|
2808
|
+
// Update grid overlay bounds after aspect ratio change
|
|
2809
|
+
previewContainer.post(() -> updateGridOverlayBounds());
|
|
2810
|
+
}
|
|
2811
|
+
} catch (NumberFormatException e) {
|
|
2812
|
+
Log.e(TAG, "Invalid aspect ratio format: " + aspectRatio, e);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
private int getWebViewTopInset() {
|
|
2817
|
+
try {
|
|
2818
|
+
if (webView != null) {
|
|
2819
|
+
// Get the actual WebView position on screen
|
|
2820
|
+
int[] location = new int[2];
|
|
2821
|
+
webView.getLocationOnScreen(location);
|
|
2822
|
+
return location[1]; // Y position is the top inset
|
|
2823
|
+
}
|
|
2824
|
+
} catch (Exception e) {
|
|
2825
|
+
Log.w(TAG, "Failed to get WebView top inset", e);
|
|
2826
|
+
}
|
|
2827
|
+
return 0;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
private int getWebViewLeftInset() {
|
|
2831
|
+
try {
|
|
2832
|
+
if (webView != null) {
|
|
2833
|
+
// Get the actual WebView position on screen for consistency
|
|
2834
|
+
int[] location = new int[2];
|
|
2835
|
+
webView.getLocationOnScreen(location);
|
|
2836
|
+
return location[0]; // X position is the left inset
|
|
2837
|
+
}
|
|
2838
|
+
} catch (Exception e) {
|
|
2839
|
+
Log.w(TAG, "Failed to get WebView left inset", e);
|
|
2840
|
+
}
|
|
2841
|
+
return 0;
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
/**
|
|
2845
|
+
* Get the current preview position and size in DP units (without insets)
|
|
2846
|
+
*/
|
|
2847
|
+
public int[] getCurrentPreviewBounds() {
|
|
2848
|
+
if (previewContainer == null) {
|
|
2849
|
+
return new int[] { 0, 0, 0, 0 }; // x, y, width, height
|
|
883
2850
|
}
|
|
884
2851
|
|
|
885
|
-
|
|
886
|
-
|
|
2852
|
+
// Get actual camera preview bounds (accounts for letterboxing/pillarboxing)
|
|
2853
|
+
int actualX = getPreviewX();
|
|
2854
|
+
int actualY = getPreviewY();
|
|
2855
|
+
int actualWidth = getPreviewWidth();
|
|
2856
|
+
int actualHeight = getPreviewHeight();
|
|
2857
|
+
|
|
2858
|
+
// Convert to logical pixels for JavaScript
|
|
2859
|
+
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
|
2860
|
+
float pixelRatio = metrics.density;
|
|
2861
|
+
|
|
2862
|
+
// Remove WebView insets from coordinates
|
|
2863
|
+
int webViewTopInset = getWebViewTopInset();
|
|
2864
|
+
int webViewLeftInset = getWebViewLeftInset();
|
|
2865
|
+
|
|
2866
|
+
int x = Math.max(
|
|
2867
|
+
0,
|
|
2868
|
+
(int) ((actualX - webViewLeftInset) / pixelRatio)
|
|
2869
|
+
);
|
|
2870
|
+
int y = Math.max(
|
|
2871
|
+
0,
|
|
2872
|
+
(int) ((actualY - webViewTopInset) / pixelRatio)
|
|
2873
|
+
);
|
|
2874
|
+
int width = (int) (actualWidth / pixelRatio);
|
|
2875
|
+
int height = (int) (actualHeight / pixelRatio);
|
|
2876
|
+
|
|
2877
|
+
return new int[] { x, y, width, height };
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
private void updateGridOverlayBounds() {
|
|
2881
|
+
if (gridOverlayView != null && previewView != null) {
|
|
2882
|
+
// Get the actual camera bounds
|
|
2883
|
+
Rect cameraBounds = getActualCameraBounds();
|
|
2884
|
+
|
|
2885
|
+
// Update the grid overlay with the camera bounds
|
|
2886
|
+
gridOverlayView.setCameraBounds(cameraBounds);
|
|
2887
|
+
|
|
2888
|
+
Log.d(
|
|
2889
|
+
TAG,
|
|
2890
|
+
"updateGridOverlayBounds: Updated grid bounds to " +
|
|
2891
|
+
cameraBounds.toString()
|
|
2892
|
+
);
|
|
887
2893
|
}
|
|
2894
|
+
}
|
|
888
2895
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
2896
|
+
private void triggerAutoFocus() {
|
|
2897
|
+
if (camera == null) {
|
|
2898
|
+
return;
|
|
2899
|
+
}
|
|
892
2900
|
|
|
893
|
-
|
|
894
|
-
try {
|
|
895
|
-
// Standard physical device selection logic...
|
|
896
|
-
List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
|
|
897
|
-
CameraInfo targetCameraInfo = null;
|
|
898
|
-
for (CameraInfo cameraInfo : cameraInfos) {
|
|
899
|
-
if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
|
|
900
|
-
targetCameraInfo = cameraInfo;
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
2901
|
+
Log.d(TAG, "triggerAutoFocus: Triggering autofocus at center");
|
|
904
2902
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
// This filter will receive a list of all cameras and must return the one we want.
|
|
910
|
-
|
|
911
|
-
currentCameraSelector = new CameraSelector.Builder()
|
|
912
|
-
.addCameraFilter(cameras -> {
|
|
913
|
-
// This filter will receive a list of all cameras and must return the one we want.
|
|
914
|
-
return Collections.singletonList(finalTarget);
|
|
915
|
-
}).build();
|
|
916
|
-
currentDeviceId = deviceId;
|
|
917
|
-
bindCameraUseCases(); // Rebind with the new, highly specific selector
|
|
918
|
-
} else {
|
|
919
|
-
Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
|
|
920
|
-
}
|
|
921
|
-
} catch (Exception e) {
|
|
922
|
-
Log.e(TAG, "switchToDevice: Error switching camera", e);
|
|
923
|
-
}
|
|
924
|
-
});
|
|
2903
|
+
// Cancel any ongoing focus operation
|
|
2904
|
+
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
2905
|
+
Log.d(TAG, "triggerAutoFocus: Cancelling previous focus operation");
|
|
2906
|
+
currentFocusFuture.cancel(true);
|
|
925
2907
|
}
|
|
926
2908
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
// Determine current position based on session config and flip it
|
|
931
|
-
String currentPosition = sessionConfig.getPosition();
|
|
932
|
-
String newPosition = "front".equals(currentPosition) ? "rear" : "front";
|
|
933
|
-
|
|
934
|
-
Log.d(TAG, "flipCamera: Switching from " + currentPosition + " to " + newPosition);
|
|
935
|
-
|
|
936
|
-
sessionConfig = new CameraSessionConfiguration(
|
|
937
|
-
null, // deviceId - clear device ID to force position-based selection
|
|
938
|
-
newPosition, // position
|
|
939
|
-
sessionConfig.getX(), // x
|
|
940
|
-
sessionConfig.getY(), // y
|
|
941
|
-
sessionConfig.getWidth(), // width
|
|
942
|
-
sessionConfig.getHeight(), // height
|
|
943
|
-
sessionConfig.getPaddingBottom(), // paddingBottom
|
|
944
|
-
sessionConfig.isToBack(), // toBack
|
|
945
|
-
sessionConfig.isStoreToFile(), // storeToFile
|
|
946
|
-
sessionConfig.isEnableOpacity(), // enableOpacity
|
|
947
|
-
sessionConfig.isEnableZoom(), // enableZoom
|
|
948
|
-
sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
|
|
949
|
-
sessionConfig.isDisableAudio(), // disableAudio
|
|
950
|
-
sessionConfig.getZoomFactor() // zoomFactor
|
|
951
|
-
);
|
|
952
|
-
|
|
953
|
-
// Clear current device ID to force position-based selection
|
|
954
|
-
currentDeviceId = null;
|
|
2909
|
+
// Focus on the center of the view
|
|
2910
|
+
int viewWidth = previewView.getWidth();
|
|
2911
|
+
int viewHeight = previewView.getHeight();
|
|
955
2912
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
currentCameraSelector = buildCameraSelector();
|
|
959
|
-
bindCameraUseCases();
|
|
960
|
-
});
|
|
2913
|
+
if (viewWidth == 0 || viewHeight == 0) {
|
|
2914
|
+
return;
|
|
961
2915
|
}
|
|
962
2916
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
2917
|
+
// Create MeteringPoint at the center of the preview
|
|
2918
|
+
MeteringPointFactory factory = previewView.getMeteringPointFactory();
|
|
2919
|
+
MeteringPoint point = factory.createPoint(viewWidth / 2f, viewHeight / 2f);
|
|
2920
|
+
|
|
2921
|
+
// Create focus and metering action
|
|
2922
|
+
FocusMeteringAction action = new FocusMeteringAction.Builder(
|
|
2923
|
+
point,
|
|
2924
|
+
FocusMeteringAction.FLAG_AF | FocusMeteringAction.FLAG_AE
|
|
2925
|
+
)
|
|
2926
|
+
.setAutoCancelDuration(3, TimeUnit.SECONDS) // Auto-cancel after 3 seconds
|
|
2927
|
+
.build();
|
|
2928
|
+
|
|
2929
|
+
try {
|
|
2930
|
+
currentFocusFuture = camera
|
|
2931
|
+
.getCameraControl()
|
|
2932
|
+
.startFocusAndMetering(action);
|
|
2933
|
+
currentFocusFuture.addListener(
|
|
2934
|
+
() -> {
|
|
2935
|
+
try {
|
|
2936
|
+
FocusMeteringResult result = currentFocusFuture.get();
|
|
2937
|
+
Log.d(
|
|
2938
|
+
TAG,
|
|
2939
|
+
"triggerAutoFocus: Focus completed successfully: " +
|
|
2940
|
+
result.isFocusSuccessful()
|
|
2941
|
+
);
|
|
2942
|
+
} catch (Exception e) {
|
|
2943
|
+
// Handle cancellation gracefully - this is expected when rapid operations occur
|
|
2944
|
+
if (
|
|
2945
|
+
e.getMessage() != null &&
|
|
2946
|
+
(e
|
|
2947
|
+
.getMessage()
|
|
2948
|
+
.contains("Cancelled by another startFocusAndMetering") ||
|
|
2949
|
+
e.getMessage().contains("OperationCanceledException") ||
|
|
2950
|
+
e
|
|
2951
|
+
.getClass()
|
|
2952
|
+
.getSimpleName()
|
|
2953
|
+
.contains("OperationCanceledException"))
|
|
2954
|
+
) {
|
|
2955
|
+
Log.d(
|
|
2956
|
+
TAG,
|
|
2957
|
+
"triggerAutoFocus: Auto-focus was cancelled by a newer focus request"
|
|
2958
|
+
);
|
|
2959
|
+
} else {
|
|
2960
|
+
Log.e(TAG, "triggerAutoFocus: Error during focus", e);
|
|
2961
|
+
}
|
|
2962
|
+
} finally {
|
|
2963
|
+
// Clear the reference if this is still the current operation
|
|
2964
|
+
if (currentFocusFuture != null && currentFocusFuture.isDone()) {
|
|
2965
|
+
currentFocusFuture = null;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2969
|
+
ContextCompat.getMainExecutor(context)
|
|
2970
|
+
);
|
|
2971
|
+
} catch (Exception e) {
|
|
2972
|
+
currentFocusFuture = null;
|
|
2973
|
+
Log.e(TAG, "triggerAutoFocus: Failed to trigger autofocus", e);
|
|
967
2974
|
}
|
|
2975
|
+
}
|
|
968
2976
|
}
|