capacitor-camera-multicapture 0.12.2
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/CameraMultiCapture.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +551 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraConfig.java +45 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraConfigMapper.java +98 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraMultiCapture.java +10 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraMultiCapturePlugin.java +1340 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ExifWrapper.java +108 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/GenericUploadWorker.java +173 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ImageUtils.java +215 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/PluginVersion.java +6 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ThumbnailGenerator.java +48 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/esm/controllers/camera-controller.d.ts +127 -0
- package/dist/esm/controllers/camera-controller.js +388 -0
- package/dist/esm/controllers/camera-controller.js.map +1 -0
- package/dist/esm/controllers/gallery-controller.d.ts +66 -0
- package/dist/esm/controllers/gallery-controller.js +333 -0
- package/dist/esm/controllers/gallery-controller.js.map +1 -0
- package/dist/esm/definitions.d.ts +372 -0
- package/dist/esm/definitions.js +6 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/overlay-manager.d.ts +124 -0
- package/dist/esm/overlay-manager.js +732 -0
- package/dist/esm/overlay-manager.js.map +1 -0
- package/dist/esm/overlay.d.ts +2 -0
- package/dist/esm/overlay.js +39 -0
- package/dist/esm/overlay.js.map +1 -0
- package/dist/esm/registration.d.ts +5 -0
- package/dist/esm/registration.js +8 -0
- package/dist/esm/registration.js.map +1 -0
- package/dist/esm/types/ui-types.d.ts +78 -0
- package/dist/esm/types/ui-types.js +2 -0
- package/dist/esm/types/ui-types.js.map +1 -0
- package/dist/esm/ui/capture-gesture-handler.d.ts +15 -0
- package/dist/esm/ui/capture-gesture-handler.js +87 -0
- package/dist/esm/ui/capture-gesture-handler.js.map +1 -0
- package/dist/esm/ui/default-styles.d.ts +27 -0
- package/dist/esm/ui/default-styles.js +134 -0
- package/dist/esm/ui/default-styles.js.map +1 -0
- package/dist/esm/ui/image-editor.d.ts +10 -0
- package/dist/esm/ui/image-editor.js +103 -0
- package/dist/esm/ui/image-editor.js.map +1 -0
- package/dist/esm/ui/layout-manager.d.ts +42 -0
- package/dist/esm/ui/layout-manager.js +199 -0
- package/dist/esm/ui/layout-manager.js.map +1 -0
- package/dist/esm/ui/media-viewer.d.ts +2 -0
- package/dist/esm/ui/media-viewer.js +267 -0
- package/dist/esm/ui/media-viewer.js.map +1 -0
- package/dist/esm/ui/pinch-zoom-handler.d.ts +37 -0
- package/dist/esm/ui/pinch-zoom-handler.js +118 -0
- package/dist/esm/ui/pinch-zoom-handler.js.map +1 -0
- package/dist/esm/ui/ui-factory.d.ts +39 -0
- package/dist/esm/ui/ui-factory.js +272 -0
- package/dist/esm/ui/ui-factory.js.map +1 -0
- package/dist/esm/version.d.ts +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/esm/web.d.ts +69 -0
- package/dist/esm/web.js +122 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +6274 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +6276 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/CameraMultiCapturePlugin/CameraMultiCapture.swift +8 -0
- package/ios/Sources/CameraMultiCapturePlugin/CameraMultiCapturePlugin.swift +1551 -0
- package/ios/Sources/CameraMultiCapturePlugin/PluginVersion.swift +2 -0
- package/ios/Tests/CameraMultiCapturePluginTests/CameraMultiCapturePluginTests.swift +15 -0
- package/package.json +93 -0
|
@@ -0,0 +1,1340 @@
|
|
|
1
|
+
package dev.hemang.cameramulticapture;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.graphics.Color;
|
|
5
|
+
import android.content.ContentResolver;
|
|
6
|
+
import android.content.ContentValues;
|
|
7
|
+
import android.content.Context;
|
|
8
|
+
import android.net.Uri;
|
|
9
|
+
import android.os.Build;
|
|
10
|
+
import android.os.Environment;
|
|
11
|
+
import android.provider.MediaStore;
|
|
12
|
+
import android.util.Base64;
|
|
13
|
+
import android.util.Log;
|
|
14
|
+
import android.util.Size;
|
|
15
|
+
import android.view.OrientationEventListener;
|
|
16
|
+
import android.view.Surface;
|
|
17
|
+
import android.view.ViewGroup;
|
|
18
|
+
import android.widget.FrameLayout;
|
|
19
|
+
import android.media.MediaMetadataRetriever;
|
|
20
|
+
|
|
21
|
+
import androidx.annotation.NonNull;
|
|
22
|
+
import androidx.camera.core.Camera;
|
|
23
|
+
import androidx.camera.core.CameraInfo;
|
|
24
|
+
import androidx.camera.core.CameraSelector;
|
|
25
|
+
import androidx.camera.core.ImageCapture;
|
|
26
|
+
import androidx.camera.core.ImageCaptureException;
|
|
27
|
+
import androidx.camera.core.Preview;
|
|
28
|
+
import androidx.camera.lifecycle.ProcessCameraProvider;
|
|
29
|
+
import androidx.camera.video.FileOutputOptions;
|
|
30
|
+
import androidx.camera.video.FallbackStrategy;
|
|
31
|
+
import androidx.camera.video.PendingRecording;
|
|
32
|
+
import androidx.camera.video.Quality;
|
|
33
|
+
import androidx.camera.video.QualitySelector;
|
|
34
|
+
import androidx.camera.video.Recorder;
|
|
35
|
+
import androidx.camera.video.Recording;
|
|
36
|
+
import androidx.camera.video.VideoCapture;
|
|
37
|
+
import androidx.camera.video.VideoRecordEvent;
|
|
38
|
+
import androidx.camera.view.PreviewView;
|
|
39
|
+
import androidx.core.content.ContextCompat;
|
|
40
|
+
|
|
41
|
+
import android.hardware.camera2.CameraCharacteristics;
|
|
42
|
+
import android.hardware.camera2.CameraManager;
|
|
43
|
+
import androidx.camera.camera2.interop.Camera2CameraInfo;
|
|
44
|
+
|
|
45
|
+
import com.getcapacitor.Plugin;
|
|
46
|
+
import com.getcapacitor.PluginCall;
|
|
47
|
+
import com.getcapacitor.PluginMethod;
|
|
48
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
49
|
+
import com.getcapacitor.annotation.Permission;
|
|
50
|
+
import com.getcapacitor.annotation.PermissionCallback;
|
|
51
|
+
import com.getcapacitor.PermissionState;
|
|
52
|
+
import com.getcapacitor.JSObject;
|
|
53
|
+
|
|
54
|
+
import java.io.File;
|
|
55
|
+
import java.io.FileInputStream;
|
|
56
|
+
import java.io.IOException;
|
|
57
|
+
import java.io.InputStream;
|
|
58
|
+
import java.io.OutputStream;
|
|
59
|
+
import java.nio.file.Files;
|
|
60
|
+
import java.util.concurrent.Executor;
|
|
61
|
+
import java.util.Arrays;
|
|
62
|
+
import java.util.List;
|
|
63
|
+
import java.util.ArrayList;
|
|
64
|
+
|
|
65
|
+
import org.json.JSONArray;
|
|
66
|
+
import androidx.work.*;
|
|
67
|
+
import java.util.concurrent.TimeUnit;
|
|
68
|
+
import java.util.UUID;
|
|
69
|
+
|
|
70
|
+
@CapacitorPlugin(
|
|
71
|
+
name = "CameraMultiCapture",
|
|
72
|
+
permissions = {
|
|
73
|
+
@Permission(strings = {Manifest.permission.CAMERA}, alias = "camera"),
|
|
74
|
+
@Permission(strings = {Manifest.permission.RECORD_AUDIO}, alias = "audio")
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
public class CameraMultiCapturePlugin extends Plugin {
|
|
78
|
+
|
|
79
|
+
private PreviewView previewView;
|
|
80
|
+
private ImageCapture imageCapture;
|
|
81
|
+
private VideoCapture<Recorder> videoCapture;
|
|
82
|
+
private Recording activeRecording;
|
|
83
|
+
private PluginCall pendingVideoStopCall;
|
|
84
|
+
private JSObject autoStoppedVideoResult;
|
|
85
|
+
private File currentVideoFile;
|
|
86
|
+
private Camera camera;
|
|
87
|
+
private ProcessCameraProvider cameraProvider;
|
|
88
|
+
private CameraConfig currentConfig = new CameraConfig();
|
|
89
|
+
private OrientationEventListener orientationEventListener;
|
|
90
|
+
private int lastKnownOrientation = 0; // 0=portrait, 90=landscape-left, 180=upside-down, 270=landscape-right
|
|
91
|
+
private boolean torchEnabled = false;
|
|
92
|
+
|
|
93
|
+
private void ensurePreviewView() {
|
|
94
|
+
if (previewView != null) return;
|
|
95
|
+
|
|
96
|
+
previewView = new PreviewView(getContext());
|
|
97
|
+
|
|
98
|
+
android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
|
|
99
|
+
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
|
100
|
+
float density = displayMetrics.density;
|
|
101
|
+
|
|
102
|
+
FrameLayout.LayoutParams params;
|
|
103
|
+
if (currentConfig.previewWidth == ViewGroup.LayoutParams.MATCH_PARENT &&
|
|
104
|
+
currentConfig.previewHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
|
|
105
|
+
params = new FrameLayout.LayoutParams(
|
|
106
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
107
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
int widthInPixels = (int) (currentConfig.previewWidth * density);
|
|
111
|
+
int heightInPixels = (int) (currentConfig.previewHeight * density);
|
|
112
|
+
|
|
113
|
+
params = new FrameLayout.LayoutParams(widthInPixels, heightInPixels);
|
|
114
|
+
|
|
115
|
+
params.leftMargin = (int) (currentConfig.previewX * density);
|
|
116
|
+
params.topMargin = (int) (currentConfig.previewY * density);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
previewView.setLayoutParams(params);
|
|
120
|
+
previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
|
|
121
|
+
|
|
122
|
+
if (previewView.getParent() != null) {
|
|
123
|
+
((ViewGroup) previewView.getParent()).removeView(previewView);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
ViewGroup rootView = getActivity().findViewById(android.R.id.content);
|
|
127
|
+
rootView.addView(previewView, 0);
|
|
128
|
+
|
|
129
|
+
if (bridge != null && bridge.getWebView() != null) {
|
|
130
|
+
bridge.getWebView().setBackgroundColor(Color.TRANSPARENT);
|
|
131
|
+
bridge.getWebView().setAlpha(1.0f);
|
|
132
|
+
bridge.getWebView().bringToFront();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
rootView.requestLayout();
|
|
136
|
+
rootView.invalidate();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private void bindCameraSession() {
|
|
140
|
+
if (cameraProvider == null || previewView == null) {
|
|
141
|
+
Log.e("CameraMultiCapture", "Camera provider or previewView is null");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Preserve current zoom level before rebuilding session
|
|
146
|
+
if (camera != null) {
|
|
147
|
+
androidx.camera.core.ZoomState zoomState = camera.getCameraInfo().getZoomState().getValue();
|
|
148
|
+
if (zoomState != null) {
|
|
149
|
+
currentConfig.zoomRatio = zoomState.getZoomRatio();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
cameraProvider.unbindAll();
|
|
154
|
+
|
|
155
|
+
Preview preview = new Preview.Builder().build();
|
|
156
|
+
preview.setSurfaceProvider(previewView.getSurfaceProvider());
|
|
157
|
+
|
|
158
|
+
// Swap resolution dimensions for portrait orientations
|
|
159
|
+
Size finalResolution = currentConfig.resolution;
|
|
160
|
+
if (currentConfig.targetRotation == Surface.ROTATION_0 || currentConfig.targetRotation == Surface.ROTATION_180) {
|
|
161
|
+
// Portrait orientations - ensure height > width
|
|
162
|
+
if (currentConfig.resolution.getWidth() > currentConfig.resolution.getHeight()) {
|
|
163
|
+
finalResolution = new Size(currentConfig.resolution.getHeight(), currentConfig.resolution.getWidth());
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Landscape orientations - ensure width > height
|
|
167
|
+
if (currentConfig.resolution.getHeight() > currentConfig.resolution.getWidth()) {
|
|
168
|
+
finalResolution = new Size(currentConfig.resolution.getHeight(), currentConfig.resolution.getWidth());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
imageCapture = new ImageCapture.Builder()
|
|
173
|
+
.setCaptureMode(currentConfig.captureMode)
|
|
174
|
+
.setTargetRotation(currentConfig.targetRotation)
|
|
175
|
+
.setTargetResolution(finalResolution)
|
|
176
|
+
.setFlashMode(currentConfig.flashMode)
|
|
177
|
+
.build();
|
|
178
|
+
|
|
179
|
+
Recorder recorder = new Recorder.Builder()
|
|
180
|
+
.setQualitySelector(
|
|
181
|
+
QualitySelector.fromOrderedList(
|
|
182
|
+
Arrays.asList(Quality.FHD, Quality.HD, Quality.SD),
|
|
183
|
+
FallbackStrategy.lowerQualityOrHigherThan(Quality.SD)
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
.build();
|
|
187
|
+
videoCapture = VideoCapture.withOutput(recorder);
|
|
188
|
+
|
|
189
|
+
CameraSelector cameraSelector = new CameraSelector.Builder()
|
|
190
|
+
.requireLensFacing(currentConfig.lensFacing)
|
|
191
|
+
.build();
|
|
192
|
+
|
|
193
|
+
camera = cameraProvider.bindToLifecycle(
|
|
194
|
+
getActivity(),
|
|
195
|
+
cameraSelector,
|
|
196
|
+
preview,
|
|
197
|
+
imageCapture,
|
|
198
|
+
videoCapture
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
camera.getCameraControl().setZoomRatio(currentConfig.zoomRatio);
|
|
202
|
+
previewView.setKeepScreenOn(true);
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@PluginMethod
|
|
207
|
+
public void start(PluginCall call) {
|
|
208
|
+
// Check permissions before starting camera
|
|
209
|
+
if (getPermissionState("camera") != PermissionState.GRANTED) {
|
|
210
|
+
call.reject("Camera permission not granted. Please call requestPermissions() first.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
currentConfig = CameraConfigMapper.fromJSObject(call.getData());
|
|
215
|
+
|
|
216
|
+
startOrientationListener();
|
|
217
|
+
|
|
218
|
+
if (!call.hasOption("rotation")) {
|
|
219
|
+
int sensorOrientation = getRotationFromOrientation(lastKnownOrientation);
|
|
220
|
+
currentConfig.targetRotation = sensorOrientation;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getActivity().runOnUiThread(() -> {
|
|
224
|
+
ensurePreviewView();
|
|
225
|
+
ProcessCameraProvider.getInstance(getContext()).addListener(() -> {
|
|
226
|
+
try {
|
|
227
|
+
cameraProvider = ProcessCameraProvider.getInstance(getContext()).get();
|
|
228
|
+
bindCameraSession();
|
|
229
|
+
call.resolve();
|
|
230
|
+
} catch (Exception e) {
|
|
231
|
+
call.reject("Failed to bind camera session: " + e.getMessage(), e);
|
|
232
|
+
}
|
|
233
|
+
}, ContextCompat.getMainExecutor(getContext()));
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
@PluginMethod
|
|
238
|
+
public void stop(PluginCall call) {
|
|
239
|
+
getActivity().runOnUiThread(() -> {
|
|
240
|
+
try {
|
|
241
|
+
stopOrientationListener();
|
|
242
|
+
|
|
243
|
+
if (camera != null) {
|
|
244
|
+
try {
|
|
245
|
+
camera.getCameraControl().enableTorch(false);
|
|
246
|
+
} catch (Exception ignored) { /* best effort */ }
|
|
247
|
+
}
|
|
248
|
+
torchEnabled = false;
|
|
249
|
+
|
|
250
|
+
if (activeRecording != null) {
|
|
251
|
+
activeRecording.stop();
|
|
252
|
+
activeRecording = null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (cameraProvider != null) {
|
|
256
|
+
cameraProvider.unbindAll();
|
|
257
|
+
cameraProvider = null;
|
|
258
|
+
}
|
|
259
|
+
if (previewView != null) {
|
|
260
|
+
ViewGroup parent = (ViewGroup) previewView.getParent();
|
|
261
|
+
if (parent != null) {
|
|
262
|
+
parent.removeView(previewView);
|
|
263
|
+
}
|
|
264
|
+
previewView = null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
camera = null;
|
|
268
|
+
imageCapture = null;
|
|
269
|
+
videoCapture = null;
|
|
270
|
+
pendingVideoStopCall = null;
|
|
271
|
+
autoStoppedVideoResult = null;
|
|
272
|
+
currentVideoFile = null;
|
|
273
|
+
|
|
274
|
+
call.resolve();
|
|
275
|
+
} catch (Exception e) {
|
|
276
|
+
call.reject("Failed to stop camera: " + e.getMessage(), e);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Saves an image file to the device's gallery using MediaStore API.
|
|
283
|
+
* Works on Android 10+ (API 29+) with scoped storage.
|
|
284
|
+
*
|
|
285
|
+
* @param imageFile The image file to save
|
|
286
|
+
* @param albumName The album/folder name in Pictures directory
|
|
287
|
+
* @return The content URI of the saved image, or null if failed
|
|
288
|
+
*/
|
|
289
|
+
private Uri saveImageToGallery(File imageFile, String albumName) {
|
|
290
|
+
ContentResolver resolver = getContext().getContentResolver();
|
|
291
|
+
ContentValues contentValues = new ContentValues();
|
|
292
|
+
|
|
293
|
+
String fileName = "IMG_" + System.currentTimeMillis() + ".jpg";
|
|
294
|
+
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
|
|
295
|
+
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
|
|
296
|
+
contentValues.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis() / 1000);
|
|
297
|
+
contentValues.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
|
|
298
|
+
|
|
299
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
300
|
+
contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/" + albumName);
|
|
301
|
+
contentValues.put(MediaStore.Images.Media.IS_PENDING, 1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
Uri imageUri = null;
|
|
305
|
+
OutputStream outputStream = null;
|
|
306
|
+
InputStream inputStream = null;
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
|
|
310
|
+
|
|
311
|
+
if (imageUri == null) {
|
|
312
|
+
Log.e("CameraMultiCapture", "Failed to create MediaStore entry");
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
outputStream = resolver.openOutputStream(imageUri);
|
|
317
|
+
if (outputStream == null) {
|
|
318
|
+
Log.e("CameraMultiCapture", "Failed to open output stream");
|
|
319
|
+
resolver.delete(imageUri, null, null);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
inputStream = new FileInputStream(imageFile);
|
|
324
|
+
byte[] buffer = new byte[8192];
|
|
325
|
+
int bytesRead;
|
|
326
|
+
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
327
|
+
outputStream.write(buffer, 0, bytesRead);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
outputStream.flush();
|
|
331
|
+
|
|
332
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
333
|
+
contentValues.clear();
|
|
334
|
+
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0);
|
|
335
|
+
resolver.update(imageUri, contentValues, null, null);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
Log.d("CameraMultiCapture", "Image saved to gallery: " + imageUri.toString());
|
|
339
|
+
return imageUri;
|
|
340
|
+
|
|
341
|
+
} catch (Exception e) {
|
|
342
|
+
Log.e("CameraMultiCapture", "Failed to save image to gallery: " + e.getMessage(), e);
|
|
343
|
+
if (imageUri != null) {
|
|
344
|
+
try {
|
|
345
|
+
resolver.delete(imageUri, null, null);
|
|
346
|
+
} catch (Exception deleteEx) {
|
|
347
|
+
Log.e("CameraMultiCapture", "Failed to clean up failed gallery entry", deleteEx);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
} finally {
|
|
352
|
+
try {
|
|
353
|
+
if (outputStream != null) outputStream.close();
|
|
354
|
+
if (inputStream != null) inputStream.close();
|
|
355
|
+
} catch (IOException e) {
|
|
356
|
+
Log.e("CameraMultiCapture", "Error closing streams", e);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
@PluginMethod
|
|
362
|
+
public void capture(PluginCall call) {
|
|
363
|
+
if (imageCapture == null) {
|
|
364
|
+
call.reject("ImageCapture not initialized");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Turn off torch before capture when flash is enabled; the LED is shared
|
|
369
|
+
// hardware so an active torch prevents the flash from firing correctly.
|
|
370
|
+
if (currentConfig.flashMode != ImageCapture.FLASH_MODE_OFF && torchEnabled && camera != null) {
|
|
371
|
+
try {
|
|
372
|
+
camera.getCameraControl().enableTorch(false);
|
|
373
|
+
torchEnabled = false;
|
|
374
|
+
} catch (Exception e) {
|
|
375
|
+
Log.w("CameraMultiCapture", "Failed to turn off torch before flash capture: " + e.getMessage());
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
int quality = call.getInt("quality", currentConfig.jpegQuality);
|
|
380
|
+
//Log.d("CameraMultiCapture", "Capture quality: " + quality + ", saveToGallery: " + currentConfig.saveToGallery);
|
|
381
|
+
|
|
382
|
+
int sensorOrientation = getRotationFromOrientation(lastKnownOrientation);
|
|
383
|
+
imageCapture.setTargetRotation(sensorOrientation);
|
|
384
|
+
currentConfig.targetRotation = sensorOrientation;
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
File photoFile = new File(getContext().getCacheDir(), "photo_" + System.currentTimeMillis() + ".jpg");
|
|
388
|
+
ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(photoFile).build();
|
|
389
|
+
|
|
390
|
+
imageCapture.takePicture(
|
|
391
|
+
outputOptions,
|
|
392
|
+
ContextCompat.getMainExecutor(getContext()),
|
|
393
|
+
new ImageCapture.OnImageSavedCallback() {
|
|
394
|
+
@Override
|
|
395
|
+
public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
|
|
396
|
+
JSObject result = new JSObject();
|
|
397
|
+
JSObject imageData = new JSObject();
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
boolean orientationCorrected = ImageUtils.correctImageOrientation(photoFile);
|
|
401
|
+
if (!orientationCorrected) {
|
|
402
|
+
Log.w("CameraMultiCapture", "Failed to correct image orientation");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
Uri uri = Uri.fromFile(photoFile);
|
|
406
|
+
imageData.put("uri", uri.toString());
|
|
407
|
+
|
|
408
|
+
// Save to gallery if enabled (default: false)
|
|
409
|
+
if (currentConfig.enableSaving) {
|
|
410
|
+
Uri galleryUri = saveImageToGallery(photoFile, currentConfig.galleryAlbumName);
|
|
411
|
+
if (galleryUri != null) {
|
|
412
|
+
imageData.put("galleryUri", galleryUri.toString());
|
|
413
|
+
Log.d("CameraMultiCapture", "Image saved to gallery: " + galleryUri.toString());
|
|
414
|
+
} else {
|
|
415
|
+
Log.w("CameraMultiCapture", "Failed to save image to gallery, but capture succeeded");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
String thumbnailBase64 = ThumbnailGenerator.generateThumbnail(photoFile);
|
|
420
|
+
if (thumbnailBase64 != null) {
|
|
421
|
+
imageData.put("thumbnail", thumbnailBase64);
|
|
422
|
+
} else {
|
|
423
|
+
Log.w("CameraMultiCapture", "Thumbnail generation failed");
|
|
424
|
+
imageData.put("thumbnail", "");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
result.put("value", imageData);
|
|
428
|
+
} catch (Exception e) {
|
|
429
|
+
call.reject("Failed to process photo file", e);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
call.resolve(result);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
@Override
|
|
436
|
+
public void onError(@NonNull ImageCaptureException exception) {
|
|
437
|
+
//Log.e("CameraMultiCapture", "=== CAPTURE ERROR: " + exception.getMessage() + " ===", exception);
|
|
438
|
+
call.reject("Photo capture failed: " + exception.getMessage());
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
);
|
|
442
|
+
} catch (Exception e) {
|
|
443
|
+
call.reject("Capture error: " + e.getMessage(), e);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
@PluginMethod
|
|
448
|
+
public void startVideoRecording(PluginCall call) {
|
|
449
|
+
if (videoCapture == null) {
|
|
450
|
+
call.reject("VideoCapture not initialized");
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (activeRecording != null) {
|
|
454
|
+
call.reject("Video recording is already in progress");
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (getPermissionState("audio") != PermissionState.GRANTED) {
|
|
458
|
+
call.reject("Microphone permission not granted. Please call requestPermissions() first.");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
int sensorOrientation = getRotationFromOrientation(lastKnownOrientation);
|
|
463
|
+
currentConfig.targetRotation = sensorOrientation;
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
currentVideoFile = new File(getContext().getCacheDir(), "video_" + System.currentTimeMillis() + ".mp4");
|
|
467
|
+
FileOutputOptions.Builder outputOptionsBuilder = new FileOutputOptions.Builder(currentVideoFile);
|
|
468
|
+
if (currentConfig != null && currentConfig.maxRecordingDurationSeconds > 0) {
|
|
469
|
+
outputOptionsBuilder.setDurationLimitMillis(currentConfig.maxRecordingDurationSeconds * 1000L);
|
|
470
|
+
}
|
|
471
|
+
FileOutputOptions outputOptions = outputOptionsBuilder.build();
|
|
472
|
+
PendingRecording pendingRecording = videoCapture.getOutput()
|
|
473
|
+
.prepareRecording(getContext(), outputOptions)
|
|
474
|
+
.withAudioEnabled();
|
|
475
|
+
|
|
476
|
+
activeRecording = pendingRecording.start(
|
|
477
|
+
ContextCompat.getMainExecutor(getContext()),
|
|
478
|
+
event -> {
|
|
479
|
+
if (event instanceof VideoRecordEvent.Finalize finalizeEvent) {
|
|
480
|
+
handleVideoFinalize(finalizeEvent);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
if (currentConfig.flashMode == ImageCapture.FLASH_MODE_ON && camera != null) {
|
|
486
|
+
camera.getCameraControl().enableTorch(true);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
call.resolve();
|
|
490
|
+
} catch (Exception e) {
|
|
491
|
+
call.reject("Failed to start video recording: " + e.getMessage(), e);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
@PluginMethod
|
|
496
|
+
public void stopVideoRecording(PluginCall call) {
|
|
497
|
+
// If recording was already auto-stopped (e.g. by maxRecordingDuration),
|
|
498
|
+
// return the buffered result immediately.
|
|
499
|
+
if (autoStoppedVideoResult != null) {
|
|
500
|
+
JSObject buffered = autoStoppedVideoResult;
|
|
501
|
+
autoStoppedVideoResult = null;
|
|
502
|
+
call.resolve(buffered);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (activeRecording == null) {
|
|
507
|
+
call.reject("No active video recording to stop");
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
pendingVideoStopCall = call;
|
|
511
|
+
activeRecording.stop();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private void handleVideoFinalize(VideoRecordEvent.Finalize finalizeEvent) {
|
|
515
|
+
try {
|
|
516
|
+
if (camera != null) {
|
|
517
|
+
camera.getCameraControl().enableTorch(false);
|
|
518
|
+
}
|
|
519
|
+
} catch (Exception ignored) { /* best effort */ }
|
|
520
|
+
|
|
521
|
+
PluginCall call = pendingVideoStopCall;
|
|
522
|
+
pendingVideoStopCall = null;
|
|
523
|
+
|
|
524
|
+
Recording recording = activeRecording;
|
|
525
|
+
activeRecording = null;
|
|
526
|
+
if (recording != null) {
|
|
527
|
+
recording.close();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// When durationLimitMillis is reached, CameraX reports
|
|
531
|
+
// ERROR_DURATION_LIMIT_REACHED. The video file is still valid,
|
|
532
|
+
// so we treat it as a successful recording.
|
|
533
|
+
if (finalizeEvent.hasError()
|
|
534
|
+
&& finalizeEvent.getError() != VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED) {
|
|
535
|
+
if (call != null) {
|
|
536
|
+
call.reject("Video recording failed: " + finalizeEvent.getError());
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
Uri outputUri = finalizeEvent.getOutputResults().getOutputUri();
|
|
542
|
+
if (outputUri == null || outputUri.toString().isEmpty()) {
|
|
543
|
+
outputUri = currentVideoFile != null ? Uri.fromFile(currentVideoFile) : null;
|
|
544
|
+
}
|
|
545
|
+
if (outputUri == null) {
|
|
546
|
+
if (call != null) {
|
|
547
|
+
call.reject("Video recording completed but no output URI is available");
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (currentConfig.enableSaving) {
|
|
553
|
+
saveVideoToGallery(outputUri);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
String thumbnail = generateVideoThumbnail(outputUri);
|
|
557
|
+
double duration = getVideoDurationSeconds(outputUri);
|
|
558
|
+
|
|
559
|
+
JSObject result = new JSObject();
|
|
560
|
+
JSObject videoData = new JSObject();
|
|
561
|
+
videoData.put("uri", outputUri.toString());
|
|
562
|
+
videoData.put("thumbnail", thumbnail != null ? thumbnail : "");
|
|
563
|
+
videoData.put("duration", duration);
|
|
564
|
+
result.put("value", videoData);
|
|
565
|
+
|
|
566
|
+
if (call != null) {
|
|
567
|
+
call.resolve(result);
|
|
568
|
+
} else {
|
|
569
|
+
// Recording was auto-stopped by maxRecordingDuration.
|
|
570
|
+
// Buffer the result for the next stopVideoRecording call.
|
|
571
|
+
autoStoppedVideoResult = result;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private String generateVideoThumbnail(Uri videoUri) {
|
|
576
|
+
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
577
|
+
try {
|
|
578
|
+
retriever.setDataSource(getContext(), videoUri);
|
|
579
|
+
android.graphics.Bitmap bitmap = retriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
|
580
|
+
if (bitmap == null) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream();
|
|
584
|
+
bitmap.compress(android.graphics.Bitmap.CompressFormat.JPEG, 85, stream);
|
|
585
|
+
byte[] bytes = stream.toByteArray();
|
|
586
|
+
return "data:image/jpeg;base64," + Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
587
|
+
} catch (Exception e) {
|
|
588
|
+
Log.w("CameraMultiCapture", "Failed to generate video thumbnail: " + e.getMessage());
|
|
589
|
+
return null;
|
|
590
|
+
} finally {
|
|
591
|
+
try {
|
|
592
|
+
retriever.release();
|
|
593
|
+
} catch (IOException ignored) {
|
|
594
|
+
// best effort cleanup
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private double getVideoDurationSeconds(Uri videoUri) {
|
|
600
|
+
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
|
601
|
+
try {
|
|
602
|
+
retriever.setDataSource(getContext(), videoUri);
|
|
603
|
+
String durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
|
|
604
|
+
if (durationMs == null) {
|
|
605
|
+
return 0;
|
|
606
|
+
}
|
|
607
|
+
return Double.parseDouble(durationMs) / 1000.0;
|
|
608
|
+
} catch (Exception e) {
|
|
609
|
+
return 0;
|
|
610
|
+
} finally {
|
|
611
|
+
try {
|
|
612
|
+
retriever.release();
|
|
613
|
+
} catch (IOException ignored) {
|
|
614
|
+
// best effort cleanup
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private void saveVideoToGallery(Uri videoUri) {
|
|
620
|
+
try {
|
|
621
|
+
ContentResolver resolver = getContext().getContentResolver();
|
|
622
|
+
ContentValues values = new ContentValues();
|
|
623
|
+
values.put(MediaStore.Video.Media.DISPLAY_NAME, "VID_" + System.currentTimeMillis() + ".mp4");
|
|
624
|
+
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
|
|
625
|
+
|
|
626
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
627
|
+
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES);
|
|
628
|
+
values.put(MediaStore.Video.Media.IS_PENDING, 1);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
Uri galleryUri = resolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
|
|
632
|
+
if (galleryUri == null) {
|
|
633
|
+
Log.w("CameraMultiCapture", "Failed to create MediaStore entry for video");
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
File sourceFile = new File(videoUri.getPath());
|
|
638
|
+
try (OutputStream out = resolver.openOutputStream(galleryUri);
|
|
639
|
+
java.io.InputStream in = Files.newInputStream(sourceFile.toPath())) {
|
|
640
|
+
if (out == null) {
|
|
641
|
+
Log.w("CameraMultiCapture", "Failed to open output stream for gallery video");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
byte[] buffer = new byte[8192];
|
|
645
|
+
int bytesRead;
|
|
646
|
+
while ((bytesRead = in.read(buffer)) != -1) {
|
|
647
|
+
out.write(buffer, 0, bytesRead);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
652
|
+
values.clear();
|
|
653
|
+
values.put(MediaStore.Video.Media.IS_PENDING, 0);
|
|
654
|
+
resolver.update(galleryUri, values, null, null);
|
|
655
|
+
}
|
|
656
|
+
} catch (Exception e) {
|
|
657
|
+
Log.w("CameraMultiCapture", "Failed to save video to gallery: " + e.getMessage());
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
@PluginMethod
|
|
662
|
+
public void setZoom(PluginCall call) {
|
|
663
|
+
float zoom;
|
|
664
|
+
if (call.hasOption("zoom")) {
|
|
665
|
+
Double zoomValue = call.getDouble("zoom");
|
|
666
|
+
zoom = zoomValue != null ? zoomValue.floatValue() : currentConfig.zoomRatio;
|
|
667
|
+
} else {
|
|
668
|
+
zoom = currentConfig.zoomRatio;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
getActivity().runOnUiThread(() -> {
|
|
672
|
+
if (camera != null) {
|
|
673
|
+
// Get the zoom state to determine min and max zoom ratios
|
|
674
|
+
@SuppressWarnings("UnusedVariable")
|
|
675
|
+
androidx.camera.core.ZoomState zoomState = camera.getCameraInfo().getZoomState().getValue();
|
|
676
|
+
if (zoomState != null) {
|
|
677
|
+
float minZoom = zoomState.getMinZoomRatio();
|
|
678
|
+
float maxZoom = zoomState.getMaxZoomRatio();
|
|
679
|
+
// Clamp zoom to valid range
|
|
680
|
+
float clampedZoom = Math.max(minZoom, Math.min(zoom, maxZoom));
|
|
681
|
+
currentConfig.zoomRatio = clampedZoom;
|
|
682
|
+
camera.getCameraControl().setZoomRatio(clampedZoom);
|
|
683
|
+
call.resolve();
|
|
684
|
+
} else {
|
|
685
|
+
// Fallback if zoom state is not available
|
|
686
|
+
currentConfig.zoomRatio = zoom;
|
|
687
|
+
camera.getCameraControl().setZoomRatio(zoom);
|
|
688
|
+
call.resolve();
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
call.reject("Camera not initialized");
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
@PluginMethod
|
|
697
|
+
public void getAvailableZoomLevels(PluginCall call) {
|
|
698
|
+
getActivity().runOnUiThread(() -> {
|
|
699
|
+
if (camera != null) {
|
|
700
|
+
androidx.camera.core.ZoomState zoomState = camera.getCameraInfo().getZoomState().getValue();
|
|
701
|
+
if (zoomState != null) {
|
|
702
|
+
float minZoom = zoomState.getMinZoomRatio();
|
|
703
|
+
float maxZoom = zoomState.getMaxZoomRatio();
|
|
704
|
+
|
|
705
|
+
// Generate suggested preset levels based on device capabilities
|
|
706
|
+
JSObject result = new JSObject();
|
|
707
|
+
result.put("minZoom", minZoom);
|
|
708
|
+
result.put("maxZoom", maxZoom);
|
|
709
|
+
|
|
710
|
+
// Create preset levels array
|
|
711
|
+
List<Float> presetLevels = new ArrayList<>();
|
|
712
|
+
|
|
713
|
+
// Add ultra-wide if available (0.5x or 0.7x depending on device)
|
|
714
|
+
if (minZoom < 1.0f) {
|
|
715
|
+
// Round to nearest common value (0.5 or 0.7)
|
|
716
|
+
float ultraWide = minZoom;
|
|
717
|
+
if (minZoom > 0.6f && minZoom < 0.8f) {
|
|
718
|
+
ultraWide = 0.7f;
|
|
719
|
+
} else if (minZoom < 0.6f) {
|
|
720
|
+
ultraWide = 0.5f;
|
|
721
|
+
}
|
|
722
|
+
presetLevels.add(ultraWide);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Always add 1x
|
|
726
|
+
presetLevels.add(1.0f);
|
|
727
|
+
|
|
728
|
+
// Add telephoto presets based on max zoom
|
|
729
|
+
if (maxZoom >= 2.0f) {
|
|
730
|
+
presetLevels.add(2.0f);
|
|
731
|
+
}
|
|
732
|
+
if (maxZoom >= 3.0f) {
|
|
733
|
+
presetLevels.add(3.0f);
|
|
734
|
+
}
|
|
735
|
+
if (maxZoom >= 5.0f) {
|
|
736
|
+
presetLevels.add(5.0f);
|
|
737
|
+
}
|
|
738
|
+
if (maxZoom >= 10.0f) {
|
|
739
|
+
presetLevels.add(10.0f);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Convert to JSON array
|
|
743
|
+
JSONArray presetArray = new JSONArray();
|
|
744
|
+
for (Float level : presetLevels) {
|
|
745
|
+
presetArray.put(level);
|
|
746
|
+
}
|
|
747
|
+
result.put("presetLevels", presetArray);
|
|
748
|
+
|
|
749
|
+
call.resolve(result);
|
|
750
|
+
} else {
|
|
751
|
+
// Fallback if zoom state is not available
|
|
752
|
+
JSObject result = new JSObject();
|
|
753
|
+
result.put("minZoom", 1.0f);
|
|
754
|
+
result.put("maxZoom", 4.0f);
|
|
755
|
+
result.put("presetLevels", new JSONArray(Arrays.asList(1.0f, 2.0f, 3.0f, 4.0f)));
|
|
756
|
+
call.resolve(result);
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
call.reject("Camera not initialized");
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
@PluginMethod
|
|
765
|
+
public void queueBackgroundUpload(PluginCall call) {
|
|
766
|
+
String imageUri = call.getString("imageUri");
|
|
767
|
+
String uploadEndpoint = call.getString("uploadEndpoint");
|
|
768
|
+
JSObject headers = call.getObject("headers");
|
|
769
|
+
JSObject formData = call.getObject("formData", new JSObject());
|
|
770
|
+
String method = call.getString("method", "POST");
|
|
771
|
+
Boolean deleteAfterUpload = call.getBoolean("deleteAfterUpload", true); // Default: true
|
|
772
|
+
|
|
773
|
+
if (imageUri == null || uploadEndpoint == null || headers == null) {
|
|
774
|
+
call.reject("Missing required parameters");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
String jobId = UUID.randomUUID().toString();
|
|
779
|
+
String uniqueFileName = generateUniqueFileName(imageUri);
|
|
780
|
+
|
|
781
|
+
Data inputData = new Data.Builder()
|
|
782
|
+
.putString("jobId", jobId)
|
|
783
|
+
.putString("imageUri", imageUri)
|
|
784
|
+
.putString("uploadEndpoint", uploadEndpoint)
|
|
785
|
+
.putString("headers", headers.toString())
|
|
786
|
+
.putString("formData", formData.toString())
|
|
787
|
+
.putString("method", method)
|
|
788
|
+
.putString("fileName", uniqueFileName)
|
|
789
|
+
.putBoolean("deleteAfterUpload", deleteAfterUpload)
|
|
790
|
+
.build();
|
|
791
|
+
|
|
792
|
+
OneTimeWorkRequest uploadWork = new OneTimeWorkRequest.Builder(GenericUploadWorker.class)
|
|
793
|
+
.setInputData(inputData)
|
|
794
|
+
.addTag(jobId)
|
|
795
|
+
.setConstraints(new Constraints.Builder()
|
|
796
|
+
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
797
|
+
.build())
|
|
798
|
+
.build();
|
|
799
|
+
|
|
800
|
+
WorkManager.getInstance(getContext()).enqueue(uploadWork);
|
|
801
|
+
|
|
802
|
+
JSObject result = new JSObject();
|
|
803
|
+
result.put("jobId", jobId);
|
|
804
|
+
call.resolve(result);
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
@PluginMethod
|
|
808
|
+
public void getUploadStatus(PluginCall call) {
|
|
809
|
+
String jobId = call.getString("jobId");
|
|
810
|
+
if (jobId == null) {
|
|
811
|
+
call.reject("Missing jobId parameter");
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
try {
|
|
816
|
+
WorkManager workManager = WorkManager.getInstance(getContext());
|
|
817
|
+
List<WorkInfo> workInfoList = workManager.getWorkInfosByTag(jobId).get();
|
|
818
|
+
JSObject result = new JSObject();
|
|
819
|
+
|
|
820
|
+
if (workInfoList.isEmpty()) {
|
|
821
|
+
result.put("status", "failed");
|
|
822
|
+
result.put("error", "Job not found");
|
|
823
|
+
} else {
|
|
824
|
+
WorkInfo workInfo = workInfoList.get(0);
|
|
825
|
+
WorkInfo.State state = workInfo.getState();
|
|
826
|
+
|
|
827
|
+
switch (state) {
|
|
828
|
+
case ENQUEUED:
|
|
829
|
+
case BLOCKED:
|
|
830
|
+
result.put("status", "pending");
|
|
831
|
+
break;
|
|
832
|
+
case RUNNING:
|
|
833
|
+
result.put("status", "uploading");
|
|
834
|
+
break;
|
|
835
|
+
case SUCCEEDED:
|
|
836
|
+
result.put("status", "completed");
|
|
837
|
+
break;
|
|
838
|
+
case FAILED:
|
|
839
|
+
case CANCELLED:
|
|
840
|
+
result.put("status", "failed");
|
|
841
|
+
Data outputData = workInfo.getOutputData();
|
|
842
|
+
String error = outputData.getString("error");
|
|
843
|
+
if (error != null) {
|
|
844
|
+
result.put("error", error);
|
|
845
|
+
}
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
call.resolve(result);
|
|
851
|
+
} catch (Exception e) {
|
|
852
|
+
call.reject("Failed to get upload status: " + e.getMessage());
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
@PluginMethod
|
|
857
|
+
public void getVersion(PluginCall call) {
|
|
858
|
+
JSObject result = new JSObject();
|
|
859
|
+
result.put("version", PluginVersion.VERSION);
|
|
860
|
+
call.resolve(result);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private String generateUniqueFileName(String imageUri) {
|
|
864
|
+
try {
|
|
865
|
+
Uri uri = Uri.parse(imageUri);
|
|
866
|
+
String path = uri.getPath();
|
|
867
|
+
|
|
868
|
+
if (path != null && !path.isEmpty()) {
|
|
869
|
+
String fileName = new File(path).getName();
|
|
870
|
+
|
|
871
|
+
if (!fileName.isEmpty() && !fileName.equals("image.jpg")) {
|
|
872
|
+
String baseName = fileName.contains(".") ?
|
|
873
|
+
fileName.substring(0, fileName.lastIndexOf('.')) : fileName;
|
|
874
|
+
String extension = fileName.contains(".") ?
|
|
875
|
+
fileName.substring(fileName.lastIndexOf('.')) : ".jpg";
|
|
876
|
+
|
|
877
|
+
return baseName + "_" + System.currentTimeMillis() + extension;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
long timestamp = System.currentTimeMillis();
|
|
882
|
+
String randomId = UUID.randomUUID().toString().substring(0, 8);
|
|
883
|
+
return "photo_" + timestamp + "_" + randomId + ".jpg";
|
|
884
|
+
|
|
885
|
+
} catch (Exception e) {
|
|
886
|
+
return "photo_" + System.currentTimeMillis() + ".jpg";
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
@PluginMethod
|
|
891
|
+
public void switchCamera(PluginCall call) {
|
|
892
|
+
if (activeRecording != null) {
|
|
893
|
+
call.reject("Cannot switch camera while recording");
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
currentConfig.lensFacing = (currentConfig.lensFacing == CameraSelector.LENS_FACING_BACK)
|
|
898
|
+
? CameraSelector.LENS_FACING_FRONT
|
|
899
|
+
: CameraSelector.LENS_FACING_BACK;
|
|
900
|
+
|
|
901
|
+
// If we are switching to the front camera, ensure the torch is turned off
|
|
902
|
+
if (currentConfig.lensFacing == CameraSelector.LENS_FACING_FRONT && camera != null) {
|
|
903
|
+
try {
|
|
904
|
+
camera.getCameraControl().enableTorch(false);
|
|
905
|
+
} catch (Exception e) {
|
|
906
|
+
Log.w("CameraMultiCapture", "Failed to disable torch on camera switch: " + e.getMessage());
|
|
907
|
+
}
|
|
908
|
+
torchEnabled = false;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
try {
|
|
912
|
+
getActivity().runOnUiThread(() -> {
|
|
913
|
+
bindCameraSession();
|
|
914
|
+
call.resolve();
|
|
915
|
+
});
|
|
916
|
+
} catch (Exception e) {
|
|
917
|
+
call.reject("Failed to switch camera: " + e.getMessage(), e);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
@PluginMethod
|
|
922
|
+
public void getAvailableCameras(PluginCall call) {
|
|
923
|
+
JSObject result = new JSObject();
|
|
924
|
+
|
|
925
|
+
try {
|
|
926
|
+
boolean hasUltrawide = false;
|
|
927
|
+
boolean hasWide = false;
|
|
928
|
+
boolean hasTelephoto = false;
|
|
929
|
+
float ultrawideZoomFactor = 0.5f;
|
|
930
|
+
float wideZoomFactor = 1.0f;
|
|
931
|
+
float telephotoZoomFactor = 2.0f;
|
|
932
|
+
|
|
933
|
+
// Check current camera's capabilities
|
|
934
|
+
if (camera != null && camera.getCameraInfo() != null) {
|
|
935
|
+
androidx.camera.core.ZoomState zoomState = camera.getCameraInfo().getZoomState().getValue();
|
|
936
|
+
if (zoomState != null) {
|
|
937
|
+
float minZoom = zoomState.getMinZoomRatio();
|
|
938
|
+
float maxZoom = zoomState.getMaxZoomRatio();
|
|
939
|
+
|
|
940
|
+
Log.d("CameraMultiCapture", "Current camera zoom range: " + minZoom + " - " + maxZoom);
|
|
941
|
+
|
|
942
|
+
// Detect ultrawide by minimum zoom < 1.0
|
|
943
|
+
if (minZoom < 1.0f) {
|
|
944
|
+
hasUltrawide = true;
|
|
945
|
+
ultrawideZoomFactor = minZoom;
|
|
946
|
+
Log.d("CameraMultiCapture", "Ultrawide detected with zoom factor: " + ultrawideZoomFactor);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Wide camera is always available
|
|
950
|
+
hasWide = true;
|
|
951
|
+
|
|
952
|
+
// Try to get more detailed camera info using Camera2 interop
|
|
953
|
+
try {
|
|
954
|
+
Camera2CameraInfo camera2Info = Camera2CameraInfo.from(camera.getCameraInfo());
|
|
955
|
+
String cameraId = camera2Info.getCameraId();
|
|
956
|
+
CameraManager cameraManager = (CameraManager) getContext().getSystemService(Context.CAMERA_SERVICE);
|
|
957
|
+
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
|
|
958
|
+
|
|
959
|
+
// Check for physical camera IDs (multi-camera systems)
|
|
960
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
961
|
+
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
|
|
962
|
+
if (capabilities != null) {
|
|
963
|
+
for (int capability : capabilities) {
|
|
964
|
+
if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) {
|
|
965
|
+
// This is a logical camera that combines multiple physical cameras
|
|
966
|
+
Log.d("CameraMultiCapture", "Logical multi-camera detected");
|
|
967
|
+
|
|
968
|
+
// More accurate telephoto detection
|
|
969
|
+
if (maxZoom >= 5.0f) {
|
|
970
|
+
hasTelephoto = true;
|
|
971
|
+
// Detect common telephoto zoom factors
|
|
972
|
+
if (maxZoom >= 20.0f) {
|
|
973
|
+
telephotoZoomFactor = 3.0f;
|
|
974
|
+
} else if (maxZoom >= 10.0f) {
|
|
975
|
+
telephotoZoomFactor = 2.0f;
|
|
976
|
+
} else {
|
|
977
|
+
telephotoZoomFactor = 2.0f;
|
|
978
|
+
}
|
|
979
|
+
Log.d("CameraMultiCapture", "Telephoto detected with zoom factor: " + telephotoZoomFactor);
|
|
980
|
+
}
|
|
981
|
+
break;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
} else {
|
|
986
|
+
// Fallback for older devices
|
|
987
|
+
if (maxZoom >= 8.0f) {
|
|
988
|
+
hasTelephoto = true;
|
|
989
|
+
telephotoZoomFactor = 2.0f;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Get focal lengths if available
|
|
994
|
+
float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
|
|
995
|
+
if (focalLengths != null && focalLengths.length > 0) {
|
|
996
|
+
Log.d("CameraMultiCapture", "Available focal lengths: " + Arrays.toString(focalLengths));
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
} catch (Exception e) {
|
|
1000
|
+
Log.e("CameraMultiCapture", "Error getting Camera2 info: " + e.getMessage());
|
|
1001
|
+
// Fallback to simple detection
|
|
1002
|
+
if (maxZoom >= 8.0f) {
|
|
1003
|
+
hasTelephoto = true;
|
|
1004
|
+
telephotoZoomFactor = 2.0f;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
} else if (cameraProvider != null) {
|
|
1009
|
+
// Camera not initialized, try to check available cameras
|
|
1010
|
+
List<androidx.camera.core.CameraInfo> cameraInfoList = cameraProvider.getAvailableCameraInfos();
|
|
1011
|
+
for (androidx.camera.core.CameraInfo cameraInfo : cameraInfoList) {
|
|
1012
|
+
androidx.camera.core.ZoomState zoomState = cameraInfo.getZoomState().getValue();
|
|
1013
|
+
if (zoomState != null) {
|
|
1014
|
+
float minZoom = zoomState.getMinZoomRatio();
|
|
1015
|
+
float maxZoom = zoomState.getMaxZoomRatio();
|
|
1016
|
+
|
|
1017
|
+
if (minZoom < 1.0f && !hasUltrawide) {
|
|
1018
|
+
hasUltrawide = true;
|
|
1019
|
+
ultrawideZoomFactor = minZoom;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
hasWide = true;
|
|
1023
|
+
|
|
1024
|
+
if (maxZoom >= 8.0f && !hasTelephoto) {
|
|
1025
|
+
hasTelephoto = true;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
// No camera provider, return defaults
|
|
1031
|
+
hasWide = true;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
result.put("hasUltrawide", hasUltrawide);
|
|
1035
|
+
result.put("hasWide", hasWide);
|
|
1036
|
+
result.put("hasTelephoto", hasTelephoto);
|
|
1037
|
+
|
|
1038
|
+
if (hasUltrawide) {
|
|
1039
|
+
result.put("ultrawideZoomFactor", ultrawideZoomFactor);
|
|
1040
|
+
}
|
|
1041
|
+
result.put("wideZoomFactor", wideZoomFactor);
|
|
1042
|
+
if (hasTelephoto) {
|
|
1043
|
+
result.put("telephotoZoomFactor", telephotoZoomFactor);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
Log.d("CameraMultiCapture", "Camera detection result: ultrawide=" + hasUltrawide +
|
|
1047
|
+
", wide=" + hasWide + ", telephoto=" + hasTelephoto);
|
|
1048
|
+
|
|
1049
|
+
} catch (Exception e) {
|
|
1050
|
+
Log.e("CameraMultiCapture", "Error in getAvailableCameras: " + e.getMessage());
|
|
1051
|
+
// Fallback to simple detection
|
|
1052
|
+
result.put("hasUltrawide", false);
|
|
1053
|
+
result.put("hasWide", true);
|
|
1054
|
+
result.put("hasTelephoto", false);
|
|
1055
|
+
result.put("wideZoomFactor", 1.0);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
call.resolve(result);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
@PluginMethod
|
|
1062
|
+
public void switchToPhysicalCamera(PluginCall call) {
|
|
1063
|
+
if (activeRecording != null) {
|
|
1064
|
+
call.reject("Cannot switch camera while recording");
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
Float zoomFactor = call.getFloat("zoomFactor");
|
|
1069
|
+
if (zoomFactor == null) {
|
|
1070
|
+
call.reject("Missing zoomFactor parameter");
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (camera == null) {
|
|
1075
|
+
call.reject("Camera not initialized");
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
Log.d("CameraMultiCapture", "Switching to physical camera with zoom factor: " + zoomFactor);
|
|
1080
|
+
|
|
1081
|
+
// For CameraX, we primarily use zoom to switch between lenses
|
|
1082
|
+
// The system automatically switches physical cameras based on zoom level
|
|
1083
|
+
getActivity().runOnUiThread(() -> {
|
|
1084
|
+
try {
|
|
1085
|
+
camera.getCameraControl().setZoomRatio(zoomFactor);
|
|
1086
|
+
|
|
1087
|
+
// Log the actual zoom after setting
|
|
1088
|
+
androidx.camera.core.ZoomState zoomState = camera.getCameraInfo().getZoomState().getValue();
|
|
1089
|
+
if (zoomState != null) {
|
|
1090
|
+
Log.d("CameraMultiCapture", "Zoom set to: " + zoomState.getZoomRatio());
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
call.resolve();
|
|
1094
|
+
} catch (Exception e) {
|
|
1095
|
+
Log.e("CameraMultiCapture", "Failed to switch camera: " + e.getMessage());
|
|
1096
|
+
call.reject("Failed to switch camera: " + e.getMessage(), e);
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
@PluginMethod
|
|
1102
|
+
public void updatePreviewRect(PluginCall call) {
|
|
1103
|
+
JSObject previewRect = call.getData();
|
|
1104
|
+
if (previewRect == null) {
|
|
1105
|
+
call.reject("Missing previewRect data");
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Update the current config with new dimensions
|
|
1110
|
+
currentConfig.previewWidth = previewRect.getInteger("width", currentConfig.previewWidth);
|
|
1111
|
+
currentConfig.previewHeight = previewRect.getInteger("height", currentConfig.previewHeight);
|
|
1112
|
+
currentConfig.previewX = previewRect.getInteger("x", currentConfig.previewX);
|
|
1113
|
+
currentConfig.previewY = previewRect.getInteger("y", currentConfig.previewY);
|
|
1114
|
+
|
|
1115
|
+
getActivity().runOnUiThread(() -> {
|
|
1116
|
+
if (!call.hasOption("rotation")) {
|
|
1117
|
+
int deviceRotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
|
|
1118
|
+
|
|
1119
|
+
if (currentConfig.targetRotation != deviceRotation) {
|
|
1120
|
+
// Update BOTH the config AND the ImageCapture object
|
|
1121
|
+
currentConfig.targetRotation = deviceRotation;
|
|
1122
|
+
|
|
1123
|
+
// Update ImageCapture target rotation if available
|
|
1124
|
+
if (imageCapture != null) {
|
|
1125
|
+
imageCapture.setTargetRotation(deviceRotation);
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (previewView != null && cameraProvider != null) {
|
|
1131
|
+
// Update preview view layout
|
|
1132
|
+
android.util.DisplayMetrics displayMetrics = new android.util.DisplayMetrics();
|
|
1133
|
+
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
|
1134
|
+
float density = displayMetrics.density;
|
|
1135
|
+
|
|
1136
|
+
FrameLayout.LayoutParams params;
|
|
1137
|
+
if (currentConfig.previewWidth == ViewGroup.LayoutParams.MATCH_PARENT &&
|
|
1138
|
+
currentConfig.previewHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
|
|
1139
|
+
params = new FrameLayout.LayoutParams(
|
|
1140
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
1141
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
1142
|
+
);
|
|
1143
|
+
} else {
|
|
1144
|
+
int widthInPixels = (int) (currentConfig.previewWidth * density);
|
|
1145
|
+
int heightInPixels = (int) (currentConfig.previewHeight * density);
|
|
1146
|
+
|
|
1147
|
+
params = new FrameLayout.LayoutParams(widthInPixels, heightInPixels);
|
|
1148
|
+
|
|
1149
|
+
params.leftMargin = (int) (currentConfig.previewX * density);
|
|
1150
|
+
params.topMargin = (int) (currentConfig.previewY * density);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
previewView.setLayoutParams(params);
|
|
1154
|
+
previewView.requestLayout();
|
|
1155
|
+
call.resolve();
|
|
1156
|
+
} else {
|
|
1157
|
+
call.reject("Preview view not initialized");
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
@PluginMethod
|
|
1163
|
+
public void checkPermissions(PluginCall call) {
|
|
1164
|
+
JSObject result = new JSObject();
|
|
1165
|
+
|
|
1166
|
+
// Check camera permission
|
|
1167
|
+
PermissionState cameraState = getPermissionState("camera");
|
|
1168
|
+
result.put("camera", cameraState.toString());
|
|
1169
|
+
|
|
1170
|
+
// Check microphone permission
|
|
1171
|
+
PermissionState audioState = getPermissionState("audio");
|
|
1172
|
+
result.put("audio", audioState.toString());
|
|
1173
|
+
|
|
1174
|
+
call.resolve(result);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
@PluginMethod
|
|
1178
|
+
public void requestPermissions(PluginCall call) {
|
|
1179
|
+
// Only request camera + audio — plugin never reads media library.
|
|
1180
|
+
if (getPermissionState("camera") != PermissionState.GRANTED ||
|
|
1181
|
+
getPermissionState("audio") != PermissionState.GRANTED) {
|
|
1182
|
+
requestPermissionForAliases(new String[]{"camera", "audio"}, call, "permissionCallback");
|
|
1183
|
+
} else {
|
|
1184
|
+
// All permissions already granted
|
|
1185
|
+
checkPermissions(call);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
@PermissionCallback
|
|
1190
|
+
private void permissionCallback(PluginCall call) {
|
|
1191
|
+
// After permission request, return the current status
|
|
1192
|
+
checkPermissions(call);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
@PluginMethod
|
|
1196
|
+
public void setFlash(PluginCall call) {
|
|
1197
|
+
String flashMode = call.getString("flashMode");
|
|
1198
|
+
if (flashMode == null) {
|
|
1199
|
+
call.reject("Missing flashMode parameter");
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
int flashModeInt;
|
|
1204
|
+
switch (flashMode) {
|
|
1205
|
+
case "on":
|
|
1206
|
+
flashModeInt = ImageCapture.FLASH_MODE_ON;
|
|
1207
|
+
break;
|
|
1208
|
+
case "auto":
|
|
1209
|
+
flashModeInt = ImageCapture.FLASH_MODE_AUTO;
|
|
1210
|
+
break;
|
|
1211
|
+
default:
|
|
1212
|
+
flashModeInt = ImageCapture.FLASH_MODE_OFF;
|
|
1213
|
+
break;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
currentConfig.flashMode = flashModeInt;
|
|
1217
|
+
|
|
1218
|
+
getActivity().runOnUiThread(() -> {
|
|
1219
|
+
try {
|
|
1220
|
+
if (imageCapture != null) {
|
|
1221
|
+
imageCapture.setFlashMode(flashModeInt);
|
|
1222
|
+
|
|
1223
|
+
JSObject result = new JSObject();
|
|
1224
|
+
result.put("flashMode", flashMode);
|
|
1225
|
+
call.resolve(result);
|
|
1226
|
+
} else {
|
|
1227
|
+
call.reject("ImageCapture not initialized");
|
|
1228
|
+
}
|
|
1229
|
+
} catch (Exception e) {
|
|
1230
|
+
call.reject("Failed to set flash mode: " + e.getMessage(), e);
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
@PluginMethod
|
|
1236
|
+
public void getFlash(PluginCall call) {
|
|
1237
|
+
String flashMode;
|
|
1238
|
+
switch (currentConfig.flashMode) {
|
|
1239
|
+
case ImageCapture.FLASH_MODE_ON:
|
|
1240
|
+
flashMode = "on";
|
|
1241
|
+
break;
|
|
1242
|
+
case ImageCapture.FLASH_MODE_AUTO:
|
|
1243
|
+
flashMode = "auto";
|
|
1244
|
+
break;
|
|
1245
|
+
default:
|
|
1246
|
+
flashMode = "off";
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
JSObject result = new JSObject();
|
|
1251
|
+
result.put("flashMode", flashMode);
|
|
1252
|
+
call.resolve(result);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
@PluginMethod
|
|
1256
|
+
public void setTorch(PluginCall call) {
|
|
1257
|
+
Boolean enabled = call.getBoolean("enabled");
|
|
1258
|
+
if (enabled == null) {
|
|
1259
|
+
call.reject("Missing enabled parameter");
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
getActivity().runOnUiThread(() -> {
|
|
1264
|
+
try {
|
|
1265
|
+
if (camera == null) {
|
|
1266
|
+
call.reject("Camera not initialized");
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Torch is controlled via CameraX CameraControl to ensure proper lifecycle handling.
|
|
1271
|
+
camera.getCameraControl().enableTorch(enabled);
|
|
1272
|
+
torchEnabled = enabled;
|
|
1273
|
+
call.resolve();
|
|
1274
|
+
} catch (Exception e) {
|
|
1275
|
+
call.reject("Failed to set torch: " + e.getMessage(), e);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
@PluginMethod
|
|
1281
|
+
public void getTorch(PluginCall call) {
|
|
1282
|
+
JSObject result = new JSObject();
|
|
1283
|
+
result.put("enabled", torchEnabled);
|
|
1284
|
+
call.resolve(result);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
private void startOrientationListener() {
|
|
1288
|
+
if (orientationEventListener == null) {
|
|
1289
|
+
orientationEventListener = new OrientationEventListener(getContext()) {
|
|
1290
|
+
@Override
|
|
1291
|
+
public void onOrientationChanged(int orientation) {
|
|
1292
|
+
if (orientation == ORIENTATION_UNKNOWN) return;
|
|
1293
|
+
|
|
1294
|
+
int newOrientation;
|
|
1295
|
+
if (orientation >= 315 || orientation < 45) {
|
|
1296
|
+
newOrientation = 0;
|
|
1297
|
+
} else if (orientation >= 45 && orientation < 135) {
|
|
1298
|
+
newOrientation = 270;
|
|
1299
|
+
} else if (orientation >= 135 && orientation < 225) {
|
|
1300
|
+
newOrientation = 180;
|
|
1301
|
+
} else {
|
|
1302
|
+
newOrientation = 90;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (newOrientation != lastKnownOrientation) {
|
|
1306
|
+
lastKnownOrientation = newOrientation;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (orientationEventListener.canDetectOrientation()) {
|
|
1313
|
+
orientationEventListener.enable();
|
|
1314
|
+
} else {
|
|
1315
|
+
Log.w("CameraMultiCapture", "Cannot detect device orientation");
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
private void stopOrientationListener() {
|
|
1320
|
+
if (orientationEventListener != null) {
|
|
1321
|
+
orientationEventListener.disable();
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
private int getRotationFromOrientation(int orientation) {
|
|
1326
|
+
switch (orientation) {
|
|
1327
|
+
case 0:
|
|
1328
|
+
return Surface.ROTATION_0;
|
|
1329
|
+
case 90:
|
|
1330
|
+
return Surface.ROTATION_90;
|
|
1331
|
+
case 180:
|
|
1332
|
+
return Surface.ROTATION_180;
|
|
1333
|
+
case 270:
|
|
1334
|
+
return Surface.ROTATION_270;
|
|
1335
|
+
default:
|
|
1336
|
+
return Surface.ROTATION_0;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
}
|