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