@capgo/camera-preview 7.23.9 → 7.23.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -18
- package/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +6 -6
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraPreview.java +1833 -2289
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java +3146 -3946
- package/android/src/main/java/app/capgo/capacitor/camera/preview/GridOverlayView.java +86 -97
- package/android/src/main/java/app/capgo/capacitor/camera/preview/model/CameraDevice.java +53 -53
- package/android/src/main/java/app/capgo/capacitor/camera/preview/model/CameraSessionConfiguration.java +166 -166
- package/android/src/main/java/app/capgo/capacitor/camera/preview/model/LensInfo.java +22 -27
- package/android/src/main/java/app/capgo/capacitor/camera/preview/model/ZoomFactors.java +22 -22
- package/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +4 -4
- package/dist/docs.json +34 -34
- package/dist/esm/definitions.d.ts +15 -15
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +5 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +4 -4
- package/dist/esm/web.js +201 -216
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +201 -216
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +201 -216
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -2
|
@@ -50,2347 +50,1891 @@ import java.util.Objects;
|
|
|
50
50
|
import org.json.JSONObject;
|
|
51
51
|
|
|
52
52
|
@CapacitorPlugin(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@Permission(
|
|
64
|
-
strings = {
|
|
65
|
-
Manifest.permission.ACCESS_COARSE_LOCATION,
|
|
66
|
-
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
67
|
-
},
|
|
68
|
-
alias = CameraPreview.CAMERA_WITH_LOCATION_PERMISSION_ALIAS
|
|
69
|
-
),
|
|
70
|
-
@Permission(
|
|
71
|
-
strings = { RECORD_AUDIO },
|
|
72
|
-
alias = CameraPreview.MICROPHONE_ONLY_PERMISSION_ALIAS
|
|
73
|
-
),
|
|
74
|
-
}
|
|
53
|
+
name = "CameraPreview",
|
|
54
|
+
permissions = {
|
|
55
|
+
@Permission(strings = { CAMERA, RECORD_AUDIO }, alias = CameraPreview.CAMERA_WITH_AUDIO_PERMISSION_ALIAS),
|
|
56
|
+
@Permission(strings = { CAMERA }, alias = CameraPreview.CAMERA_ONLY_PERMISSION_ALIAS),
|
|
57
|
+
@Permission(
|
|
58
|
+
strings = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION },
|
|
59
|
+
alias = CameraPreview.CAMERA_WITH_LOCATION_PERMISSION_ALIAS
|
|
60
|
+
),
|
|
61
|
+
@Permission(strings = { RECORD_AUDIO }, alias = CameraPreview.MICROPHONE_ONLY_PERMISSION_ALIAS)
|
|
62
|
+
}
|
|
75
63
|
)
|
|
76
|
-
public class CameraPreview
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
lastSessionConfig = cameraXView.getSessionConfig();
|
|
86
|
-
cameraXView.stopSession();
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
@Override
|
|
91
|
-
protected void handleOnResume() {
|
|
92
|
-
super.handleOnResume();
|
|
93
|
-
if (lastSessionConfig != null) {
|
|
94
|
-
// Recreate camera with last known configuration
|
|
95
|
-
if (cameraXView == null) {
|
|
96
|
-
cameraXView = new CameraXView(getContext(), getBridge().getWebView());
|
|
97
|
-
cameraXView.setListener(this);
|
|
98
|
-
}
|
|
99
|
-
cameraXView.startSession(lastSessionConfig);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
@Override
|
|
104
|
-
protected void handleOnDestroy() {
|
|
105
|
-
super.handleOnDestroy();
|
|
106
|
-
if (cameraXView != null) {
|
|
107
|
-
cameraXView.stopSession();
|
|
108
|
-
cameraXView = null;
|
|
109
|
-
}
|
|
110
|
-
lastSessionConfig = null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private CameraSessionConfiguration lastSessionConfig;
|
|
114
|
-
|
|
115
|
-
private static final String TAG = "CameraPreview CameraXView";
|
|
116
|
-
|
|
117
|
-
static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
|
|
118
|
-
static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
|
|
119
|
-
static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS =
|
|
120
|
-
"cameraWithLocation";
|
|
121
|
-
static final String MICROPHONE_ONLY_PERMISSION_ALIAS = "microphoneOnly";
|
|
122
|
-
|
|
123
|
-
private String captureCallbackId = "";
|
|
124
|
-
private String sampleCallbackId = "";
|
|
125
|
-
private String cameraStartCallbackId = "";
|
|
126
|
-
private final Object pendingStartLock = new Object();
|
|
127
|
-
private PluginCall pendingStartCall;
|
|
128
|
-
private int previousOrientationRequest =
|
|
129
|
-
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
|
|
130
|
-
private CameraXView cameraXView;
|
|
131
|
-
private View rotationOverlay;
|
|
132
|
-
private FusedLocationProviderClient fusedLocationClient;
|
|
133
|
-
private Location lastLocation;
|
|
134
|
-
private OrientationEventListener orientationListener;
|
|
135
|
-
private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
|
|
136
|
-
private String lastOrientationStr = "unknown";
|
|
137
|
-
private boolean lastDisableAudio = true;
|
|
138
|
-
private Drawable originalWindowBackground;
|
|
139
|
-
private boolean isCameraPermissionDialogShowing = false;
|
|
140
|
-
|
|
141
|
-
@PluginMethod
|
|
142
|
-
public void getExposureModes(PluginCall call) {
|
|
143
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
144
|
-
call.reject("Camera is not running");
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
JSArray arr = new JSArray();
|
|
148
|
-
for (String m : cameraXView.getExposureModes()) arr.put(m);
|
|
149
|
-
JSObject ret = new JSObject();
|
|
150
|
-
ret.put("modes", arr);
|
|
151
|
-
call.resolve(ret);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
@PluginMethod
|
|
155
|
-
public void getExposureMode(PluginCall call) {
|
|
156
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
157
|
-
call.reject("Camera is not running");
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
JSObject ret = new JSObject();
|
|
161
|
-
ret.put("mode", cameraXView.getExposureMode());
|
|
162
|
-
call.resolve(ret);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
@PluginMethod
|
|
166
|
-
public void setExposureMode(PluginCall call) {
|
|
167
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
168
|
-
call.reject("Camera is not running");
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
String mode = call.getString("mode");
|
|
172
|
-
if (mode == null || mode.isEmpty()) {
|
|
173
|
-
call.reject("mode parameter is required");
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
try {
|
|
177
|
-
cameraXView.setExposureMode(mode);
|
|
178
|
-
call.resolve();
|
|
179
|
-
} catch (Exception e) {
|
|
180
|
-
call.reject("Failed to set exposure mode: " + e.getMessage());
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
@PluginMethod
|
|
185
|
-
public void getExposureCompensationRange(PluginCall call) {
|
|
186
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
187
|
-
call.reject("Camera is not running");
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
float[] range = cameraXView.getExposureCompensationRange();
|
|
192
|
-
JSObject ret = new JSObject();
|
|
193
|
-
ret.put("min", range[0]);
|
|
194
|
-
ret.put("max", range[1]);
|
|
195
|
-
ret.put("step", range.length > 2 ? range[2] : 0.1);
|
|
196
|
-
call.resolve(ret);
|
|
197
|
-
} catch (Exception e) {
|
|
198
|
-
call.reject(
|
|
199
|
-
"Failed to get exposure compensation range: " + e.getMessage()
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
@PluginMethod
|
|
205
|
-
public void getExposureCompensation(PluginCall call) {
|
|
206
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
207
|
-
call.reject("Camera is not running");
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
try {
|
|
211
|
-
float value = cameraXView.getExposureCompensation();
|
|
212
|
-
JSObject ret = new JSObject();
|
|
213
|
-
ret.put("value", value);
|
|
214
|
-
call.resolve(ret);
|
|
215
|
-
} catch (Exception e) {
|
|
216
|
-
call.reject("Failed to get exposure compensation: " + e.getMessage());
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
@PluginMethod
|
|
221
|
-
public void setExposureCompensation(PluginCall call) {
|
|
222
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
223
|
-
call.reject("Camera is not running");
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
Float value = call.getFloat("value");
|
|
227
|
-
if (value == null) {
|
|
228
|
-
call.reject("value parameter is required");
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
cameraXView.setExposureCompensation(value);
|
|
233
|
-
call.resolve();
|
|
234
|
-
} catch (Exception e) {
|
|
235
|
-
call.reject("Failed to set exposure compensation: " + e.getMessage());
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
@PluginMethod
|
|
240
|
-
public void getOrientation(PluginCall call) {
|
|
241
|
-
String o = getDeviceOrientationString();
|
|
242
|
-
JSObject ret = new JSObject();
|
|
243
|
-
ret.put("orientation", o);
|
|
244
|
-
call.resolve(ret);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
@PluginMethod
|
|
248
|
-
public void start(PluginCall call) {
|
|
249
|
-
// Prevent starting while an existing view is still active or stopping
|
|
250
|
-
if (cameraXView != null) {
|
|
251
|
-
try {
|
|
252
|
-
if (cameraXView.isRunning() && !cameraXView.isStopping()) {
|
|
253
|
-
call.reject("Camera is already running");
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
if (cameraXView.isStopping() || cameraXView.isBusy()) {
|
|
257
|
-
if (enqueuePendingStart(call)) {
|
|
258
|
-
Log.d(
|
|
259
|
-
TAG,
|
|
260
|
-
"start: Camera busy; queued start request until stop completes"
|
|
261
|
-
);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
call.reject("Camera is busy or stopping. Please retry shortly.");
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
} catch (Exception ignored) {}
|
|
268
|
-
}
|
|
269
|
-
boolean disableAudio = Boolean.TRUE.equals(
|
|
270
|
-
call.getBoolean("disableAudio", true)
|
|
271
|
-
);
|
|
272
|
-
String permissionAlias = disableAudio
|
|
273
|
-
? CAMERA_ONLY_PERMISSION_ALIAS
|
|
274
|
-
: CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
|
|
275
|
-
|
|
276
|
-
if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
|
|
277
|
-
startCamera(call);
|
|
278
|
-
} else {
|
|
279
|
-
requestPermissionForAlias(
|
|
280
|
-
permissionAlias,
|
|
281
|
-
call,
|
|
282
|
-
"handleCameraPermissionResult"
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private boolean enqueuePendingStart(PluginCall call) {
|
|
288
|
-
synchronized (pendingStartLock) {
|
|
289
|
-
if (pendingStartCall == null) {
|
|
290
|
-
pendingStartCall = call;
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
return false;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
@PluginMethod
|
|
298
|
-
public void flip(PluginCall call) {
|
|
299
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
300
|
-
call.reject("Camera is not running");
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
cameraXView.flipCamera();
|
|
304
|
-
call.resolve();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
@SuppressLint("MissingPermission")
|
|
308
|
-
@PluginMethod
|
|
309
|
-
public void capture(final PluginCall call) {
|
|
310
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
311
|
-
call.reject("Camera is not running");
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
final boolean withExifLocation = Boolean.TRUE.equals(
|
|
316
|
-
call.getBoolean("withExifLocation", false)
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
if (withExifLocation) {
|
|
320
|
-
if (
|
|
321
|
-
getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) !=
|
|
322
|
-
PermissionState.GRANTED
|
|
323
|
-
) {
|
|
324
|
-
requestPermissionForAlias(
|
|
325
|
-
CAMERA_WITH_LOCATION_PERMISSION_ALIAS,
|
|
326
|
-
call,
|
|
327
|
-
"captureWithLocationPermission"
|
|
328
|
-
);
|
|
329
|
-
} else {
|
|
330
|
-
getLocationAndCapture(call);
|
|
331
|
-
}
|
|
332
|
-
} else {
|
|
333
|
-
captureWithoutLocation(call);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
@SuppressLint("MissingPermission")
|
|
338
|
-
@PermissionCallback
|
|
339
|
-
private void captureWithLocationPermission(PluginCall call) {
|
|
340
|
-
if (
|
|
341
|
-
getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) ==
|
|
342
|
-
PermissionState.GRANTED
|
|
343
|
-
) {
|
|
344
|
-
if (
|
|
345
|
-
ActivityCompat.checkSelfPermission(
|
|
346
|
-
getContext(),
|
|
347
|
-
Manifest.permission.ACCESS_FINE_LOCATION
|
|
348
|
-
) !=
|
|
349
|
-
PackageManager.PERMISSION_GRANTED ||
|
|
350
|
-
ActivityCompat.checkSelfPermission(
|
|
351
|
-
getContext(),
|
|
352
|
-
Manifest.permission.ACCESS_COARSE_LOCATION
|
|
353
|
-
) !=
|
|
354
|
-
PackageManager.PERMISSION_GRANTED
|
|
355
|
-
) {
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
getLocationAndCapture(call);
|
|
359
|
-
} else {
|
|
360
|
-
Logger.warn(
|
|
361
|
-
"Location permission denied. Capturing photo without location data."
|
|
362
|
-
);
|
|
363
|
-
captureWithoutLocation(call);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
@RequiresPermission(
|
|
368
|
-
allOf = {
|
|
369
|
-
Manifest.permission.ACCESS_FINE_LOCATION,
|
|
370
|
-
Manifest.permission.ACCESS_COARSE_LOCATION,
|
|
371
|
-
}
|
|
372
|
-
)
|
|
373
|
-
private void getLocationAndCapture(PluginCall call) {
|
|
374
|
-
if (fusedLocationClient == null) {
|
|
375
|
-
fusedLocationClient = LocationServices.getFusedLocationProviderClient(
|
|
376
|
-
getContext()
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
fusedLocationClient
|
|
380
|
-
.getLastLocation()
|
|
381
|
-
.addOnSuccessListener(getActivity(), location -> {
|
|
382
|
-
lastLocation = location;
|
|
383
|
-
proceedWithCapture(call, lastLocation);
|
|
384
|
-
})
|
|
385
|
-
.addOnFailureListener(e -> {
|
|
386
|
-
Logger.error("Failed to get location: " + e.getMessage());
|
|
387
|
-
proceedWithCapture(call, null);
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
private void captureWithoutLocation(PluginCall call) {
|
|
392
|
-
proceedWithCapture(call, null);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
private void proceedWithCapture(PluginCall call, Location location) {
|
|
396
|
-
bridge.saveCall(call);
|
|
397
|
-
captureCallbackId = call.getCallbackId();
|
|
398
|
-
|
|
399
|
-
Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
|
|
400
|
-
final boolean saveToGallery = Boolean.TRUE.equals(
|
|
401
|
-
call.getBoolean("saveToGallery")
|
|
402
|
-
);
|
|
403
|
-
Integer width = call.getInt("width");
|
|
404
|
-
Integer height = call.getInt("height");
|
|
405
|
-
final boolean embedTimestamp = Boolean.TRUE.equals(
|
|
406
|
-
call.getBoolean("embedTimestamp")
|
|
407
|
-
);
|
|
408
|
-
final boolean embedLocation = Boolean.TRUE.equals(
|
|
409
|
-
call.getBoolean("embedLocation")
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
cameraXView.capturePhoto(
|
|
413
|
-
quality,
|
|
414
|
-
saveToGallery,
|
|
415
|
-
width,
|
|
416
|
-
height,
|
|
417
|
-
location,
|
|
418
|
-
embedTimestamp,
|
|
419
|
-
embedLocation
|
|
420
|
-
);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
@PluginMethod
|
|
424
|
-
public void captureSample(PluginCall call) {
|
|
425
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
426
|
-
call.reject("Camera is not running");
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
bridge.saveCall(call);
|
|
430
|
-
sampleCallbackId = call.getCallbackId();
|
|
431
|
-
Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
|
|
432
|
-
cameraXView.captureSample(quality);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
@PluginMethod
|
|
436
|
-
public void stop(final PluginCall call) {
|
|
437
|
-
bridge
|
|
438
|
-
.getActivity()
|
|
439
|
-
.runOnUiThread(() -> {
|
|
440
|
-
getBridge()
|
|
441
|
-
.getActivity()
|
|
442
|
-
.setRequestedOrientation(previousOrientationRequest);
|
|
443
|
-
|
|
444
|
-
// Disable and clear orientation listener
|
|
445
|
-
if (orientationListener != null) {
|
|
446
|
-
orientationListener.disable();
|
|
447
|
-
orientationListener = null;
|
|
448
|
-
lastOrientation = Configuration.ORIENTATION_UNDEFINED;
|
|
64
|
+
public class CameraPreview extends Plugin implements CameraXView.CameraXViewListener {
|
|
65
|
+
|
|
66
|
+
@Override
|
|
67
|
+
protected void handleOnPause() {
|
|
68
|
+
super.handleOnPause();
|
|
69
|
+
if (cameraXView != null && cameraXView.isRunning()) {
|
|
70
|
+
// Store the current configuration before stopping
|
|
71
|
+
lastSessionConfig = cameraXView.getSessionConfig();
|
|
72
|
+
cameraXView.stopSession();
|
|
449
73
|
}
|
|
74
|
+
}
|
|
450
75
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
76
|
+
@Override
|
|
77
|
+
protected void handleOnResume() {
|
|
78
|
+
super.handleOnResume();
|
|
79
|
+
if (lastSessionConfig != null) {
|
|
80
|
+
// Recreate camera with last known configuration
|
|
81
|
+
if (cameraXView == null) {
|
|
82
|
+
cameraXView = new CameraXView(getContext(), getBridge().getWebView());
|
|
83
|
+
cameraXView.setListener(this);
|
|
84
|
+
}
|
|
85
|
+
cameraXView.startSession(lastSessionConfig);
|
|
455
86
|
}
|
|
87
|
+
}
|
|
456
88
|
|
|
89
|
+
@Override
|
|
90
|
+
protected void handleOnDestroy() {
|
|
91
|
+
super.handleOnDestroy();
|
|
457
92
|
if (cameraXView != null) {
|
|
458
|
-
|
|
459
|
-
// Only drop the reference if no deferred stop is pending
|
|
460
|
-
if (!cameraXView.isStopDeferred()) {
|
|
93
|
+
cameraXView.stopSession();
|
|
461
94
|
cameraXView = null;
|
|
462
|
-
}
|
|
463
95
|
}
|
|
464
|
-
// Manual stops should not trigger automatic resume with stale config
|
|
465
96
|
lastSessionConfig = null;
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private CameraSessionConfiguration lastSessionConfig;
|
|
100
|
+
|
|
101
|
+
private static final String TAG = "CameraPreview CameraXView";
|
|
102
|
+
|
|
103
|
+
static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
|
|
104
|
+
static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
|
|
105
|
+
static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS = "cameraWithLocation";
|
|
106
|
+
static final String MICROPHONE_ONLY_PERMISSION_ALIAS = "microphoneOnly";
|
|
107
|
+
|
|
108
|
+
private String captureCallbackId = "";
|
|
109
|
+
private String sampleCallbackId = "";
|
|
110
|
+
private String cameraStartCallbackId = "";
|
|
111
|
+
private final Object pendingStartLock = new Object();
|
|
112
|
+
private PluginCall pendingStartCall;
|
|
113
|
+
private int previousOrientationRequest = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
|
|
114
|
+
private CameraXView cameraXView;
|
|
115
|
+
private View rotationOverlay;
|
|
116
|
+
private FusedLocationProviderClient fusedLocationClient;
|
|
117
|
+
private Location lastLocation;
|
|
118
|
+
private OrientationEventListener orientationListener;
|
|
119
|
+
private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
|
|
120
|
+
private String lastOrientationStr = "unknown";
|
|
121
|
+
private boolean lastDisableAudio = true;
|
|
122
|
+
private Drawable originalWindowBackground;
|
|
123
|
+
private boolean isCameraPermissionDialogShowing = false;
|
|
124
|
+
|
|
125
|
+
@PluginMethod
|
|
126
|
+
public void getExposureModes(PluginCall call) {
|
|
127
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
128
|
+
call.reject("Camera is not running");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
JSArray arr = new JSArray();
|
|
132
|
+
for (String m : cameraXView.getExposureModes()) arr.put(m);
|
|
133
|
+
JSObject ret = new JSObject();
|
|
134
|
+
ret.put("modes", arr);
|
|
135
|
+
call.resolve(ret);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@PluginMethod
|
|
139
|
+
public void getExposureMode(PluginCall call) {
|
|
140
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
141
|
+
call.reject("Camera is not running");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
JSObject ret = new JSObject();
|
|
145
|
+
ret.put("mode", cameraXView.getExposureMode());
|
|
146
|
+
call.resolve(ret);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@PluginMethod
|
|
150
|
+
public void setExposureMode(PluginCall call) {
|
|
151
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
152
|
+
call.reject("Camera is not running");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
String mode = call.getString("mode");
|
|
156
|
+
if (mode == null || mode.isEmpty()) {
|
|
157
|
+
call.reject("mode parameter is required");
|
|
158
|
+
return;
|
|
475
159
|
}
|
|
476
|
-
call.resolve();
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
@PluginMethod
|
|
481
|
-
public void getSupportedFlashModes(PluginCall call) {
|
|
482
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
483
|
-
call.reject("Camera is not running");
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
List<String> supportedFlashModes = cameraXView.getSupportedFlashModes();
|
|
487
|
-
JSArray jsonFlashModes = new JSArray();
|
|
488
|
-
for (String mode : supportedFlashModes) {
|
|
489
|
-
jsonFlashModes.put(mode);
|
|
490
|
-
}
|
|
491
|
-
JSObject jsObject = new JSObject();
|
|
492
|
-
jsObject.put("result", jsonFlashModes);
|
|
493
|
-
call.resolve(jsObject);
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
@PluginMethod
|
|
497
|
-
public void setFlashMode(PluginCall call) {
|
|
498
|
-
String flashMode = call.getString("flashMode");
|
|
499
|
-
if (flashMode == null || flashMode.isEmpty()) {
|
|
500
|
-
call.reject("flashMode required parameter is missing");
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
cameraXView.setFlashMode(flashMode);
|
|
504
|
-
call.resolve();
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
@PluginMethod
|
|
508
|
-
public void getAvailableDevices(PluginCall call) {
|
|
509
|
-
List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
|
|
510
|
-
getContext()
|
|
511
|
-
);
|
|
512
|
-
JSArray devicesArray = new JSArray();
|
|
513
|
-
for (CameraDevice device : devices) {
|
|
514
|
-
JSObject deviceJson = new JSObject();
|
|
515
|
-
deviceJson.put("deviceId", device.getDeviceId());
|
|
516
|
-
deviceJson.put("label", device.getLabel());
|
|
517
|
-
deviceJson.put("position", device.getPosition());
|
|
518
|
-
JSArray lensesArray = new JSArray();
|
|
519
|
-
for (app.capgo.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
|
|
520
|
-
JSObject lensJson = new JSObject();
|
|
521
|
-
lensJson.put("focalLength", lens.getFocalLength());
|
|
522
|
-
lensJson.put("deviceType", lens.getDeviceType());
|
|
523
|
-
lensJson.put("baseZoomRatio", lens.getBaseZoomRatio());
|
|
524
|
-
lensJson.put("digitalZoom", lens.getDigitalZoom());
|
|
525
|
-
lensesArray.put(lensJson);
|
|
526
|
-
}
|
|
527
|
-
deviceJson.put("lenses", lensesArray);
|
|
528
|
-
deviceJson.put("minZoom", device.getMinZoom());
|
|
529
|
-
deviceJson.put("maxZoom", device.getMaxZoom());
|
|
530
|
-
devicesArray.put(deviceJson);
|
|
531
|
-
}
|
|
532
|
-
JSObject result = new JSObject();
|
|
533
|
-
result.put("devices", devicesArray);
|
|
534
|
-
call.resolve(result);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
@PluginMethod
|
|
538
|
-
public void getZoom(PluginCall call) {
|
|
539
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
540
|
-
call.reject("Camera is not running");
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
ZoomFactors zoomFactors = cameraXView.getZoomFactors();
|
|
544
|
-
JSObject result = new JSObject();
|
|
545
|
-
result.put("min", zoomFactors.getMin());
|
|
546
|
-
result.put("max", zoomFactors.getMax());
|
|
547
|
-
result.put("current", zoomFactors.getCurrent());
|
|
548
|
-
call.resolve(result);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
@PluginMethod
|
|
552
|
-
public void getZoomButtonValues(PluginCall call) {
|
|
553
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
554
|
-
call.reject("Camera is not running");
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
// Build a sorted set to dedupe and order ascending
|
|
558
|
-
java.util.Set<Double> sorted = new java.util.TreeSet<>();
|
|
559
|
-
sorted.add(1.0);
|
|
560
|
-
sorted.add(2.0);
|
|
561
|
-
|
|
562
|
-
// Try to detect ultra-wide to include its min zoom (often 0.5)
|
|
563
|
-
try {
|
|
564
|
-
List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
|
|
565
|
-
getContext()
|
|
566
|
-
);
|
|
567
|
-
ZoomFactors zoomFactors = cameraXView.getZoomFactors();
|
|
568
|
-
boolean hasUltraWide = false;
|
|
569
|
-
boolean hasTelephoto = false;
|
|
570
|
-
float minUltra = 0.5f;
|
|
571
|
-
|
|
572
|
-
for (CameraDevice device : devices) {
|
|
573
|
-
for (app.capgo.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
|
|
574
|
-
if ("ultraWide".equals(lens.getDeviceType())) {
|
|
575
|
-
hasUltraWide = true;
|
|
576
|
-
// Use overall minZoom for that device as the button value to represent UW
|
|
577
|
-
minUltra = Math.max(minUltra, zoomFactors.getMin());
|
|
578
|
-
} else if ("telephoto".equals(lens.getDeviceType())) {
|
|
579
|
-
hasTelephoto = true;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
if (hasUltraWide) {
|
|
584
|
-
sorted.add((double) minUltra);
|
|
585
|
-
}
|
|
586
|
-
if (hasTelephoto) {
|
|
587
|
-
sorted.add(3.0);
|
|
588
|
-
}
|
|
589
|
-
} catch (Exception ignored) {
|
|
590
|
-
// Ignore and keep defaults
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
JSObject result = new JSObject();
|
|
594
|
-
JSArray values = new JSArray();
|
|
595
|
-
for (Double v : sorted) {
|
|
596
|
-
values.put(v);
|
|
597
|
-
}
|
|
598
|
-
result.put("values", values);
|
|
599
|
-
call.resolve(result);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
@PluginMethod
|
|
603
|
-
public void setZoom(PluginCall call) {
|
|
604
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
605
|
-
call.reject("Camera is not running");
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
Float level = call.getFloat("level");
|
|
609
|
-
if (level == null) {
|
|
610
|
-
call.reject("level parameter is required");
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
try {
|
|
614
|
-
cameraXView.setZoom(level);
|
|
615
|
-
call.resolve();
|
|
616
|
-
} catch (Exception e) {
|
|
617
|
-
call.reject("Failed to set zoom: " + e.getMessage());
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
@PluginMethod
|
|
622
|
-
public void setFocus(PluginCall call) {
|
|
623
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
624
|
-
call.reject("Camera is not running");
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
Float x = call.getFloat("x");
|
|
628
|
-
Float y = call.getFloat("y");
|
|
629
|
-
if (x == null || y == null) {
|
|
630
|
-
call.reject("x and y parameters are required");
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
// Reject if values are outside 0-1 range
|
|
634
|
-
if (x < 0f || x > 1f || y < 0f || y > 1f) {
|
|
635
|
-
call.reject("Focus coordinates must be between 0 and 1");
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
getActivity()
|
|
640
|
-
.runOnUiThread(() -> {
|
|
641
160
|
try {
|
|
642
|
-
|
|
643
|
-
|
|
161
|
+
cameraXView.setExposureMode(mode);
|
|
162
|
+
call.resolve();
|
|
644
163
|
} catch (Exception e) {
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
if (isCameraPermissionDialogShowing) {
|
|
775
|
-
if (completion != null) {
|
|
776
|
-
completion.run();
|
|
777
|
-
}
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
AlertDialog dialog = new AlertDialog.Builder(activity)
|
|
782
|
-
.setTitle(title)
|
|
783
|
-
.setMessage(message)
|
|
784
|
-
.setNegativeButton(cancelText, (d, which) -> {
|
|
785
|
-
d.dismiss();
|
|
786
|
-
isCameraPermissionDialogShowing = false;
|
|
787
|
-
})
|
|
788
|
-
.setPositiveButton(openSettingsText, (d, which) -> {
|
|
789
|
-
Intent intent = new Intent(
|
|
790
|
-
Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
|
791
|
-
);
|
|
792
|
-
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
|
793
|
-
intent.setData(uri);
|
|
794
|
-
activity.startActivity(intent);
|
|
795
|
-
isCameraPermissionDialogShowing = false;
|
|
796
|
-
})
|
|
797
|
-
.setOnDismissListener(d -> isCameraPermissionDialogShowing = false)
|
|
798
|
-
.create();
|
|
799
|
-
|
|
800
|
-
isCameraPermissionDialogShowing = true;
|
|
801
|
-
dialog.show();
|
|
802
|
-
if (completion != null) {
|
|
803
|
-
completion.run();
|
|
804
|
-
}
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
private String mapPermissionState(PermissionState state) {
|
|
809
|
-
if (state == null) {
|
|
810
|
-
return PermissionState.PROMPT.toString();
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
return state.toString();
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
@PluginMethod
|
|
817
|
-
public void checkPermissions(PluginCall call) {
|
|
818
|
-
boolean disableAudio = call.getBoolean("disableAudio") != null
|
|
819
|
-
? Boolean.TRUE.equals(call.getBoolean("disableAudio"))
|
|
820
|
-
: true;
|
|
821
|
-
|
|
822
|
-
PermissionState cameraState = getPermissionState(
|
|
823
|
-
CAMERA_ONLY_PERMISSION_ALIAS
|
|
824
|
-
);
|
|
825
|
-
|
|
826
|
-
JSObject result = new JSObject();
|
|
827
|
-
result.put("camera", mapPermissionState(cameraState));
|
|
828
|
-
|
|
829
|
-
if (!disableAudio) {
|
|
830
|
-
PermissionState audioState = getPermissionState(
|
|
831
|
-
MICROPHONE_ONLY_PERMISSION_ALIAS
|
|
832
|
-
);
|
|
833
|
-
result.put("microphone", mapPermissionState(audioState));
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
call.resolve(result);
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
@Override
|
|
840
|
-
@PluginMethod
|
|
841
|
-
public void requestPermissions(PluginCall call) {
|
|
842
|
-
Boolean disableAudioOption = call.getBoolean("disableAudio");
|
|
843
|
-
boolean disableAudio = disableAudioOption == null
|
|
844
|
-
? true
|
|
845
|
-
: Boolean.TRUE.equals(disableAudioOption);
|
|
846
|
-
this.lastDisableAudio = disableAudio;
|
|
847
|
-
|
|
848
|
-
String permissionAlias = disableAudio
|
|
849
|
-
? CAMERA_ONLY_PERMISSION_ALIAS
|
|
850
|
-
: CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
|
|
851
|
-
|
|
852
|
-
boolean cameraGranted = PermissionState.GRANTED.equals(
|
|
853
|
-
getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
|
|
854
|
-
);
|
|
855
|
-
boolean audioGranted =
|
|
856
|
-
disableAudio ||
|
|
857
|
-
PermissionState.GRANTED.equals(
|
|
858
|
-
getPermissionState(MICROPHONE_ONLY_PERMISSION_ALIAS)
|
|
859
|
-
);
|
|
860
|
-
|
|
861
|
-
if (cameraGranted && audioGranted) {
|
|
862
|
-
JSObject result = new JSObject();
|
|
863
|
-
result.put("camera", mapPermissionState(PermissionState.GRANTED));
|
|
864
|
-
if (!disableAudio) {
|
|
865
|
-
result.put("microphone", mapPermissionState(PermissionState.GRANTED));
|
|
866
|
-
}
|
|
867
|
-
call.resolve(result);
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
requestPermissionForAlias(
|
|
872
|
-
permissionAlias,
|
|
873
|
-
call,
|
|
874
|
-
"handleRequestPermissionsResult"
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
@PermissionCallback
|
|
879
|
-
private void handleRequestPermissionsResult(PluginCall call) {
|
|
880
|
-
Boolean disableAudioOption = call.getBoolean("disableAudio");
|
|
881
|
-
boolean disableAudio = disableAudioOption == null
|
|
882
|
-
? true
|
|
883
|
-
: Boolean.TRUE.equals(disableAudioOption);
|
|
884
|
-
this.lastDisableAudio = disableAudio;
|
|
885
|
-
|
|
886
|
-
PermissionState cameraState = getPermissionState(
|
|
887
|
-
CAMERA_ONLY_PERMISSION_ALIAS
|
|
888
|
-
);
|
|
889
|
-
JSObject result = new JSObject();
|
|
890
|
-
result.put("camera", mapPermissionState(cameraState));
|
|
891
|
-
|
|
892
|
-
if (!disableAudio) {
|
|
893
|
-
PermissionState audioState = getPermissionState(
|
|
894
|
-
CAMERA_WITH_AUDIO_PERMISSION_ALIAS
|
|
895
|
-
);
|
|
896
|
-
result.put("microphone", mapPermissionState(audioState));
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
boolean showSettingsAlert = call.getBoolean("showSettingsAlert") != null
|
|
900
|
-
? Boolean.TRUE.equals(call.getBoolean("showSettingsAlert"))
|
|
901
|
-
: false;
|
|
902
|
-
|
|
903
|
-
String cameraStateString = result.getString("camera");
|
|
904
|
-
boolean cameraNeedsSettings =
|
|
905
|
-
"denied".equals(cameraStateString) ||
|
|
906
|
-
"prompt-with-rationale".equals(cameraStateString);
|
|
907
|
-
|
|
908
|
-
boolean microphoneNeedsSettings = false;
|
|
909
|
-
if (result.has("microphone")) {
|
|
910
|
-
String micStateString = result.getString("microphone");
|
|
911
|
-
microphoneNeedsSettings =
|
|
912
|
-
"denied".equals(micStateString) ||
|
|
913
|
-
"prompt-with-rationale".equals(micStateString);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
boolean shouldShowAlert =
|
|
917
|
-
showSettingsAlert && (cameraNeedsSettings || microphoneNeedsSettings);
|
|
918
|
-
|
|
919
|
-
if (shouldShowAlert) {
|
|
920
|
-
Activity activity = getActivity();
|
|
921
|
-
if (activity == null) {
|
|
922
|
-
call.resolve(result);
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
String title = call.getString("title", "Camera Permission Needed");
|
|
927
|
-
String message = call.getString(
|
|
928
|
-
"message",
|
|
929
|
-
"Enable camera access in Settings to use the preview."
|
|
930
|
-
);
|
|
931
|
-
String openSettingsText = call.getString(
|
|
932
|
-
"openSettingsButtonTitle",
|
|
933
|
-
"Open Settings"
|
|
934
|
-
);
|
|
935
|
-
String cancelText = call.getString(
|
|
936
|
-
"cancelButtonTitle",
|
|
937
|
-
activity.getString(android.R.string.cancel)
|
|
938
|
-
);
|
|
939
|
-
|
|
940
|
-
showCameraPermissionDialog(
|
|
941
|
-
title,
|
|
942
|
-
message,
|
|
943
|
-
openSettingsText,
|
|
944
|
-
cancelText,
|
|
945
|
-
() -> call.resolve(result)
|
|
946
|
-
);
|
|
947
|
-
} else {
|
|
948
|
-
call.resolve(result);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
@PermissionCallback
|
|
953
|
-
private void handleCameraPermissionResult(PluginCall call) {
|
|
954
|
-
if (
|
|
955
|
-
PermissionState.GRANTED.equals(
|
|
956
|
-
getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
|
|
957
|
-
) ||
|
|
958
|
-
PermissionState.GRANTED.equals(
|
|
959
|
-
getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS)
|
|
960
|
-
)
|
|
961
|
-
) {
|
|
962
|
-
startCamera(call);
|
|
963
|
-
} else {
|
|
964
|
-
call.reject(
|
|
965
|
-
"camera permission denied. enable camera access in Settings.",
|
|
966
|
-
"cameraPermissionDenied"
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
private void startCamera(final PluginCall call) {
|
|
972
|
-
String positionParam = call.getString("position");
|
|
973
|
-
String originalDeviceId = call.getString("deviceId");
|
|
974
|
-
String deviceId = originalDeviceId; // Use a mutable variable
|
|
975
|
-
|
|
976
|
-
final String position = (positionParam == null ||
|
|
977
|
-
positionParam.isEmpty() ||
|
|
978
|
-
"rear".equals(positionParam) ||
|
|
979
|
-
"back".equals(positionParam))
|
|
980
|
-
? "back"
|
|
981
|
-
: "front";
|
|
982
|
-
// Use -1 as default to indicate centering is needed when x/y not provided
|
|
983
|
-
final Integer xParam = call.getInt("x");
|
|
984
|
-
final Integer yParam = call.getInt("y");
|
|
985
|
-
final int x = xParam != null ? xParam : -1;
|
|
986
|
-
final int y = yParam != null ? yParam : -1;
|
|
987
|
-
|
|
988
|
-
Log.d("CameraPreview", "========================");
|
|
989
|
-
Log.d("CameraPreview", "CAMERA POSITION TRACKING START:");
|
|
990
|
-
Log.d(
|
|
991
|
-
"CameraPreview",
|
|
992
|
-
"1. RAW PARAMS - xParam: " + xParam + ", yParam: " + yParam
|
|
993
|
-
);
|
|
994
|
-
Log.d(
|
|
995
|
-
"CameraPreview",
|
|
996
|
-
"2. AFTER DEFAULT - x: " +
|
|
997
|
-
x +
|
|
998
|
-
" (center=" +
|
|
999
|
-
(x == -1) +
|
|
1000
|
-
"), y: " +
|
|
1001
|
-
y +
|
|
1002
|
-
" (center=" +
|
|
1003
|
-
(y == -1) +
|
|
1004
|
-
")"
|
|
1005
|
-
);
|
|
1006
|
-
//noinspection DataFlowIssue
|
|
1007
|
-
final int width = call.getInt("width", 0);
|
|
1008
|
-
//noinspection DataFlowIssue
|
|
1009
|
-
final int height = call.getInt("height", 0);
|
|
1010
|
-
//noinspection DataFlowIssue
|
|
1011
|
-
final int paddingBottom = call.getInt("paddingBottom", 0);
|
|
1012
|
-
final boolean toBack = Boolean.TRUE.equals(call.getBoolean("toBack", true));
|
|
1013
|
-
final boolean storeToFile = Boolean.TRUE.equals(
|
|
1014
|
-
call.getBoolean("storeToFile", false)
|
|
1015
|
-
);
|
|
1016
|
-
final boolean enableOpacity = Boolean.TRUE.equals(
|
|
1017
|
-
call.getBoolean("enableOpacity", false)
|
|
1018
|
-
);
|
|
1019
|
-
final boolean disableExifHeaderStripping = Boolean.TRUE.equals(
|
|
1020
|
-
call.getBoolean("disableExifHeaderStripping", false)
|
|
1021
|
-
);
|
|
1022
|
-
final boolean lockOrientation = Boolean.TRUE.equals(
|
|
1023
|
-
call.getBoolean("lockAndroidOrientation", false)
|
|
1024
|
-
);
|
|
1025
|
-
final boolean disableAudio = Boolean.TRUE.equals(
|
|
1026
|
-
call.getBoolean("disableAudio", true)
|
|
1027
|
-
);
|
|
1028
|
-
this.lastDisableAudio = disableAudio;
|
|
1029
|
-
final String aspectRatio = call.getString("aspectRatio", "4:3");
|
|
1030
|
-
final String gridMode = call.getString("gridMode", "none");
|
|
1031
|
-
final String positioning = call.getString("positioning", "top");
|
|
1032
|
-
//noinspection DataFlowIssue
|
|
1033
|
-
final float initialZoomLevel = call.getFloat("initialZoomLevel", 1.0f);
|
|
1034
|
-
//noinspection DataFlowIssue
|
|
1035
|
-
final boolean disableFocusIndicator = call.getBoolean(
|
|
1036
|
-
"disableFocusIndicator",
|
|
1037
|
-
false
|
|
1038
|
-
);
|
|
1039
|
-
final boolean enableVideoMode = Boolean.TRUE.equals(
|
|
1040
|
-
call.getBoolean("enableVideoMode", false)
|
|
1041
|
-
);
|
|
1042
|
-
|
|
1043
|
-
// Check for conflict between aspectRatio and size
|
|
1044
|
-
if (
|
|
1045
|
-
call.getData().has("aspectRatio") &&
|
|
1046
|
-
(call.getData().has("width") || call.getData().has("height"))
|
|
1047
|
-
) {
|
|
1048
|
-
call.reject(
|
|
1049
|
-
"Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."
|
|
1050
|
-
);
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
float targetZoom = initialZoomLevel;
|
|
1055
|
-
// Check if the selected device is a physical ultra-wide
|
|
1056
|
-
if (originalDeviceId != null) {
|
|
1057
|
-
List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(
|
|
1058
|
-
getContext()
|
|
1059
|
-
);
|
|
1060
|
-
for (CameraDevice device : devices) {
|
|
1061
|
-
if (
|
|
1062
|
-
originalDeviceId.equals(device.getDeviceId()) && !device.isLogical()
|
|
1063
|
-
) {
|
|
1064
|
-
for (LensInfo lens : device.getLenses()) {
|
|
1065
|
-
if ("ultraWide".equals(lens.getDeviceType())) {
|
|
1066
|
-
Log.d(
|
|
1067
|
-
"CameraPreview",
|
|
1068
|
-
"Ultra-wide lens selected. Targeting 0.5x zoom on logical camera."
|
|
1069
|
-
);
|
|
1070
|
-
targetZoom = 0.5f;
|
|
1071
|
-
// Force the use of the logical camera by clearing the specific deviceId
|
|
1072
|
-
deviceId = null;
|
|
1073
|
-
break;
|
|
164
|
+
call.reject("Failed to set exposure mode: " + e.getMessage());
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@PluginMethod
|
|
169
|
+
public void getExposureCompensationRange(PluginCall call) {
|
|
170
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
171
|
+
call.reject("Camera is not running");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
float[] range = cameraXView.getExposureCompensationRange();
|
|
176
|
+
JSObject ret = new JSObject();
|
|
177
|
+
ret.put("min", range[0]);
|
|
178
|
+
ret.put("max", range[1]);
|
|
179
|
+
ret.put("step", range.length > 2 ? range[2] : 0.1);
|
|
180
|
+
call.resolve(ret);
|
|
181
|
+
} catch (Exception e) {
|
|
182
|
+
call.reject("Failed to get exposure compensation range: " + e.getMessage());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@PluginMethod
|
|
187
|
+
public void getExposureCompensation(PluginCall call) {
|
|
188
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
189
|
+
call.reject("Camera is not running");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
float value = cameraXView.getExposureCompensation();
|
|
194
|
+
JSObject ret = new JSObject();
|
|
195
|
+
ret.put("value", value);
|
|
196
|
+
call.resolve(ret);
|
|
197
|
+
} catch (Exception e) {
|
|
198
|
+
call.reject("Failed to get exposure compensation: " + e.getMessage());
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
@PluginMethod
|
|
203
|
+
public void setExposureCompensation(PluginCall call) {
|
|
204
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
205
|
+
call.reject("Camera is not running");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
Float value = call.getFloat("value");
|
|
209
|
+
if (value == null) {
|
|
210
|
+
call.reject("value parameter is required");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
cameraXView.setExposureCompensation(value);
|
|
215
|
+
call.resolve();
|
|
216
|
+
} catch (Exception e) {
|
|
217
|
+
call.reject("Failed to set exposure compensation: " + e.getMessage());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
@PluginMethod
|
|
222
|
+
public void getOrientation(PluginCall call) {
|
|
223
|
+
String o = getDeviceOrientationString();
|
|
224
|
+
JSObject ret = new JSObject();
|
|
225
|
+
ret.put("orientation", o);
|
|
226
|
+
call.resolve(ret);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
@PluginMethod
|
|
230
|
+
public void start(PluginCall call) {
|
|
231
|
+
// Prevent starting while an existing view is still active or stopping
|
|
232
|
+
if (cameraXView != null) {
|
|
233
|
+
try {
|
|
234
|
+
if (cameraXView.isRunning() && !cameraXView.isStopping()) {
|
|
235
|
+
call.reject("Camera is already running");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (cameraXView.isStopping() || cameraXView.isBusy()) {
|
|
239
|
+
if (enqueuePendingStart(call)) {
|
|
240
|
+
Log.d(TAG, "start: Camera busy; queued start request until stop completes");
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
call.reject("Camera is busy or stopping. Please retry shortly.");
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
} catch (Exception ignored) {}
|
|
247
|
+
}
|
|
248
|
+
boolean disableAudio = Boolean.TRUE.equals(call.getBoolean("disableAudio", true));
|
|
249
|
+
String permissionAlias = disableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
|
|
250
|
+
|
|
251
|
+
if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
|
|
252
|
+
startCamera(call);
|
|
253
|
+
} else {
|
|
254
|
+
requestPermissionForAlias(permissionAlias, call, "handleCameraPermissionResult");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private boolean enqueuePendingStart(PluginCall call) {
|
|
259
|
+
synchronized (pendingStartLock) {
|
|
260
|
+
if (pendingStartCall == null) {
|
|
261
|
+
pendingStartCall = call;
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@PluginMethod
|
|
269
|
+
public void flip(PluginCall call) {
|
|
270
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
271
|
+
call.reject("Camera is not running");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
cameraXView.flipCamera();
|
|
275
|
+
call.resolve();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
@SuppressLint("MissingPermission")
|
|
279
|
+
@PluginMethod
|
|
280
|
+
public void capture(final PluginCall call) {
|
|
281
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
282
|
+
call.reject("Camera is not running");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
final boolean withExifLocation = Boolean.TRUE.equals(call.getBoolean("withExifLocation", false));
|
|
287
|
+
|
|
288
|
+
if (withExifLocation) {
|
|
289
|
+
if (getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) != PermissionState.GRANTED) {
|
|
290
|
+
requestPermissionForAlias(CAMERA_WITH_LOCATION_PERMISSION_ALIAS, call, "captureWithLocationPermission");
|
|
291
|
+
} else {
|
|
292
|
+
getLocationAndCapture(call);
|
|
1074
293
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
.runOnUiThread(() -> {
|
|
1092
|
-
// Ensure transparent background when preview is behind the WebView (Android 10 fix)
|
|
1093
|
-
if (toBack) {
|
|
1094
|
-
try {
|
|
1095
|
-
if (originalWindowBackground == null) {
|
|
1096
|
-
originalWindowBackground = getBridge()
|
|
1097
|
-
.getActivity()
|
|
1098
|
-
.getWindow()
|
|
1099
|
-
.getDecorView()
|
|
1100
|
-
.getBackground();
|
|
294
|
+
} else {
|
|
295
|
+
captureWithoutLocation(call);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@SuppressLint("MissingPermission")
|
|
300
|
+
@PermissionCallback
|
|
301
|
+
private void captureWithLocationPermission(PluginCall call) {
|
|
302
|
+
if (getPermissionState(CAMERA_WITH_LOCATION_PERMISSION_ALIAS) == PermissionState.GRANTED) {
|
|
303
|
+
if (
|
|
304
|
+
ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) !=
|
|
305
|
+
PackageManager.PERMISSION_GRANTED ||
|
|
306
|
+
ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) !=
|
|
307
|
+
PackageManager.PERMISSION_GRANTED
|
|
308
|
+
) {
|
|
309
|
+
return;
|
|
1101
310
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
311
|
+
getLocationAndCapture(call);
|
|
312
|
+
} else {
|
|
313
|
+
Logger.warn("Location permission denied. Capturing photo without location data.");
|
|
314
|
+
captureWithoutLocation(call);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@RequiresPermission(allOf = { Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION })
|
|
319
|
+
private void getLocationAndCapture(PluginCall call) {
|
|
320
|
+
if (fusedLocationClient == null) {
|
|
321
|
+
fusedLocationClient = LocationServices.getFusedLocationProviderClient(getContext());
|
|
322
|
+
}
|
|
323
|
+
fusedLocationClient
|
|
324
|
+
.getLastLocation()
|
|
325
|
+
.addOnSuccessListener(getActivity(), location -> {
|
|
326
|
+
lastLocation = location;
|
|
327
|
+
proceedWithCapture(call, lastLocation);
|
|
328
|
+
})
|
|
329
|
+
.addOnFailureListener(e -> {
|
|
330
|
+
Logger.error("Failed to get location: " + e.getMessage());
|
|
331
|
+
proceedWithCapture(call, null);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private void captureWithoutLocation(PluginCall call) {
|
|
336
|
+
proceedWithCapture(call, null);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private void proceedWithCapture(PluginCall call, Location location) {
|
|
340
|
+
bridge.saveCall(call);
|
|
341
|
+
captureCallbackId = call.getCallbackId();
|
|
342
|
+
|
|
343
|
+
Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
|
|
344
|
+
final boolean saveToGallery = Boolean.TRUE.equals(call.getBoolean("saveToGallery"));
|
|
345
|
+
Integer width = call.getInt("width");
|
|
346
|
+
Integer height = call.getInt("height");
|
|
347
|
+
final boolean embedTimestamp = Boolean.TRUE.equals(call.getBoolean("embedTimestamp"));
|
|
348
|
+
final boolean embedLocation = Boolean.TRUE.equals(call.getBoolean("embedLocation"));
|
|
349
|
+
|
|
350
|
+
cameraXView.capturePhoto(quality, saveToGallery, width, height, location, embedTimestamp, embedLocation);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@PluginMethod
|
|
354
|
+
public void captureSample(PluginCall call) {
|
|
355
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
356
|
+
call.reject("Camera is not running");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
bridge.saveCall(call);
|
|
360
|
+
sampleCallbackId = call.getCallbackId();
|
|
361
|
+
Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
|
|
362
|
+
cameraXView.captureSample(quality);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@PluginMethod
|
|
366
|
+
public void stop(final PluginCall call) {
|
|
367
|
+
bridge
|
|
1114
368
|
.getActivity()
|
|
1115
|
-
.
|
|
369
|
+
.runOnUiThread(() -> {
|
|
370
|
+
getBridge().getActivity().setRequestedOrientation(previousOrientationRequest);
|
|
371
|
+
|
|
372
|
+
// Disable and clear orientation listener
|
|
373
|
+
if (orientationListener != null) {
|
|
374
|
+
orientationListener.disable();
|
|
375
|
+
orientationListener = null;
|
|
376
|
+
lastOrientation = Configuration.ORIENTATION_UNDEFINED;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Remove any rotation overlay if present
|
|
380
|
+
if (rotationOverlay != null && rotationOverlay.getParent() != null) {
|
|
381
|
+
((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
|
|
382
|
+
rotationOverlay = null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (cameraXView != null) {
|
|
386
|
+
cameraXView.stopSession();
|
|
387
|
+
// Only drop the reference if no deferred stop is pending
|
|
388
|
+
if (!cameraXView.isStopDeferred()) {
|
|
389
|
+
cameraXView = null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Manual stops should not trigger automatic resume with stale config
|
|
393
|
+
lastSessionConfig = null;
|
|
394
|
+
// Restore original window background if modified earlier
|
|
395
|
+
if (originalWindowBackground != null) {
|
|
396
|
+
try {
|
|
397
|
+
getBridge().getActivity().getWindow().setBackgroundDrawable(originalWindowBackground);
|
|
398
|
+
} catch (Exception ignored) {}
|
|
399
|
+
originalWindowBackground = null;
|
|
400
|
+
}
|
|
401
|
+
call.resolve();
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@PluginMethod
|
|
406
|
+
public void getSupportedFlashModes(PluginCall call) {
|
|
407
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
408
|
+
call.reject("Camera is not running");
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
List<String> supportedFlashModes = cameraXView.getSupportedFlashModes();
|
|
412
|
+
JSArray jsonFlashModes = new JSArray();
|
|
413
|
+
for (String mode : supportedFlashModes) {
|
|
414
|
+
jsonFlashModes.put(mode);
|
|
415
|
+
}
|
|
416
|
+
JSObject jsObject = new JSObject();
|
|
417
|
+
jsObject.put("result", jsonFlashModes);
|
|
418
|
+
call.resolve(jsObject);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
@PluginMethod
|
|
422
|
+
public void setFlashMode(PluginCall call) {
|
|
423
|
+
String flashMode = call.getString("flashMode");
|
|
424
|
+
if (flashMode == null || flashMode.isEmpty()) {
|
|
425
|
+
call.reject("flashMode required parameter is missing");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
cameraXView.setFlashMode(flashMode);
|
|
429
|
+
call.resolve();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@PluginMethod
|
|
433
|
+
public void getAvailableDevices(PluginCall call) {
|
|
434
|
+
List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(getContext());
|
|
435
|
+
JSArray devicesArray = new JSArray();
|
|
436
|
+
for (CameraDevice device : devices) {
|
|
437
|
+
JSObject deviceJson = new JSObject();
|
|
438
|
+
deviceJson.put("deviceId", device.getDeviceId());
|
|
439
|
+
deviceJson.put("label", device.getLabel());
|
|
440
|
+
deviceJson.put("position", device.getPosition());
|
|
441
|
+
JSArray lensesArray = new JSArray();
|
|
442
|
+
for (app.capgo.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
|
|
443
|
+
JSObject lensJson = new JSObject();
|
|
444
|
+
lensJson.put("focalLength", lens.getFocalLength());
|
|
445
|
+
lensJson.put("deviceType", lens.getDeviceType());
|
|
446
|
+
lensJson.put("baseZoomRatio", lens.getBaseZoomRatio());
|
|
447
|
+
lensJson.put("digitalZoom", lens.getDigitalZoom());
|
|
448
|
+
lensesArray.put(lensJson);
|
|
449
|
+
}
|
|
450
|
+
deviceJson.put("lenses", lensesArray);
|
|
451
|
+
deviceJson.put("minZoom", device.getMinZoom());
|
|
452
|
+
deviceJson.put("maxZoom", device.getMaxZoom());
|
|
453
|
+
devicesArray.put(deviceJson);
|
|
1116
454
|
}
|
|
455
|
+
JSObject result = new JSObject();
|
|
456
|
+
result.put("devices", devicesArray);
|
|
457
|
+
call.resolve(result);
|
|
458
|
+
}
|
|
1117
459
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
460
|
+
@PluginMethod
|
|
461
|
+
public void getZoom(PluginCall call) {
|
|
462
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
463
|
+
call.reject("Camera is not running");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
ZoomFactors zoomFactors = cameraXView.getZoomFactors();
|
|
467
|
+
JSObject result = new JSObject();
|
|
468
|
+
result.put("min", zoomFactors.getMin());
|
|
469
|
+
result.put("max", zoomFactors.getMax());
|
|
470
|
+
result.put("current", zoomFactors.getCurrent());
|
|
471
|
+
call.resolve(result);
|
|
472
|
+
}
|
|
1122
473
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
474
|
+
@PluginMethod
|
|
475
|
+
public void getZoomButtonValues(PluginCall call) {
|
|
476
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
477
|
+
call.reject("Camera is not running");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// Build a sorted set to dedupe and order ascending
|
|
481
|
+
java.util.Set<Double> sorted = new java.util.TreeSet<>();
|
|
482
|
+
sorted.add(1.0);
|
|
483
|
+
sorted.add(2.0);
|
|
1128
484
|
|
|
1129
|
-
|
|
1130
|
-
|
|
485
|
+
// Try to detect ultra-wide to include its min zoom (often 0.5)
|
|
486
|
+
try {
|
|
487
|
+
List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(getContext());
|
|
488
|
+
ZoomFactors zoomFactors = cameraXView.getZoomFactors();
|
|
489
|
+
boolean hasUltraWide = false;
|
|
490
|
+
boolean hasTelephoto = false;
|
|
491
|
+
float minUltra = 0.5f;
|
|
492
|
+
|
|
493
|
+
for (CameraDevice device : devices) {
|
|
494
|
+
for (app.capgo.capacitor.camera.preview.model.LensInfo lens : device.getLenses()) {
|
|
495
|
+
if ("ultraWide".equals(lens.getDeviceType())) {
|
|
496
|
+
hasUltraWide = true;
|
|
497
|
+
// Use overall minZoom for that device as the button value to represent UW
|
|
498
|
+
minUltra = Math.max(minUltra, zoomFactors.getMin());
|
|
499
|
+
} else if ("telephoto".equals(lens.getDeviceType())) {
|
|
500
|
+
hasTelephoto = true;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (hasUltraWide) {
|
|
505
|
+
sorted.add((double) minUltra);
|
|
506
|
+
}
|
|
507
|
+
if (hasTelephoto) {
|
|
508
|
+
sorted.add(3.0);
|
|
509
|
+
}
|
|
510
|
+
} catch (Exception ignored) {
|
|
511
|
+
// Ignore and keep defaults
|
|
512
|
+
}
|
|
1131
513
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
514
|
+
JSObject result = new JSObject();
|
|
515
|
+
JSArray values = new JSArray();
|
|
516
|
+
for (Double v : sorted) {
|
|
517
|
+
values.put(v);
|
|
518
|
+
}
|
|
519
|
+
result.put("values", values);
|
|
520
|
+
call.resolve(result);
|
|
521
|
+
}
|
|
1137
522
|
|
|
1138
|
-
|
|
1139
|
-
|
|
523
|
+
@PluginMethod
|
|
524
|
+
public void setZoom(PluginCall call) {
|
|
525
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
526
|
+
call.reject("Camera is not running");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
Float level = call.getFloat("level");
|
|
530
|
+
if (level == null) {
|
|
531
|
+
call.reject("level parameter is required");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
cameraXView.setZoom(level);
|
|
536
|
+
call.resolve();
|
|
537
|
+
} catch (Exception e) {
|
|
538
|
+
call.reject("Failed to set zoom: " + e.getMessage());
|
|
539
|
+
}
|
|
540
|
+
}
|
|
1140
541
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
542
|
+
@PluginMethod
|
|
543
|
+
public void setFocus(PluginCall call) {
|
|
544
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
545
|
+
call.reject("Camera is not running");
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
Float x = call.getFloat("x");
|
|
549
|
+
Float y = call.getFloat("y");
|
|
550
|
+
if (x == null || y == null) {
|
|
551
|
+
call.reject("x and y parameters are required");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// Reject if values are outside 0-1 range
|
|
555
|
+
if (x < 0f || x > 1f || y < 0f || y > 1f) {
|
|
556
|
+
call.reject("Focus coordinates must be between 0 and 1");
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
1146
559
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
")"
|
|
1158
|
-
);
|
|
1159
|
-
Log.d(
|
|
1160
|
-
"CameraPreview",
|
|
1161
|
-
" - webView locationOnScreen: (" +
|
|
1162
|
-
webViewLocationOnScreen[0] +
|
|
1163
|
-
", " +
|
|
1164
|
-
webViewLocationOnScreen[1] +
|
|
1165
|
-
")"
|
|
1166
|
-
);
|
|
1167
|
-
Log.d(
|
|
1168
|
-
"CameraPreview",
|
|
1169
|
-
" - parent locationInWindow: (" +
|
|
1170
|
-
parentLocationInWindow[0] +
|
|
1171
|
-
", " +
|
|
1172
|
-
parentLocationInWindow[1] +
|
|
1173
|
-
")"
|
|
1174
|
-
);
|
|
1175
|
-
Log.d(
|
|
1176
|
-
"CameraPreview",
|
|
1177
|
-
" - parent locationOnScreen: (" +
|
|
1178
|
-
parentLocationOnScreen[0] +
|
|
1179
|
-
", " +
|
|
1180
|
-
parentLocationOnScreen[1] +
|
|
1181
|
-
")"
|
|
1182
|
-
);
|
|
560
|
+
getActivity()
|
|
561
|
+
.runOnUiThread(() -> {
|
|
562
|
+
try {
|
|
563
|
+
cameraXView.setFocus(x, y);
|
|
564
|
+
call.resolve();
|
|
565
|
+
} catch (Exception e) {
|
|
566
|
+
call.reject("Failed to set focus: " + e.getMessage());
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
1183
570
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
if (
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
"
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
", bottom=" +
|
|
1199
|
-
marginParams.bottomMargin
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
// Check WebView padding
|
|
1204
|
-
Log.d(
|
|
1205
|
-
"CameraPreview",
|
|
1206
|
-
" - webView padding: left=" +
|
|
1207
|
-
webView.getPaddingLeft() +
|
|
1208
|
-
", top=" +
|
|
1209
|
-
webView.getPaddingTop() +
|
|
1210
|
-
", right=" +
|
|
1211
|
-
webView.getPaddingRight() +
|
|
1212
|
-
", bottom=" +
|
|
1213
|
-
webView.getPaddingBottom()
|
|
1214
|
-
);
|
|
571
|
+
@PluginMethod
|
|
572
|
+
public void setDeviceId(PluginCall call) {
|
|
573
|
+
String deviceId = call.getString("deviceId");
|
|
574
|
+
if (deviceId == null || deviceId.isEmpty()) {
|
|
575
|
+
call.reject("deviceId parameter is required");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
579
|
+
call.reject("Camera is not running");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
cameraXView.switchToDevice(deviceId);
|
|
583
|
+
call.resolve();
|
|
584
|
+
}
|
|
1215
585
|
|
|
1216
|
-
|
|
1217
|
-
|
|
586
|
+
@PluginMethod
|
|
587
|
+
public void getSupportedPictureSizes(final PluginCall call) {
|
|
588
|
+
JSArray supportedPictureSizesResult = new JSArray();
|
|
589
|
+
List<Size> rearSizes = CameraXView.getSupportedPictureSizes("rear");
|
|
590
|
+
JSObject rear = new JSObject();
|
|
591
|
+
rear.put("facing", "rear");
|
|
592
|
+
JSArray rearSizesJs = new JSArray();
|
|
593
|
+
for (Size size : rearSizes) {
|
|
594
|
+
JSObject sizeJs = new JSObject();
|
|
595
|
+
sizeJs.put("width", size.getWidth());
|
|
596
|
+
sizeJs.put("height", size.getHeight());
|
|
597
|
+
rearSizesJs.put(sizeJs);
|
|
598
|
+
}
|
|
599
|
+
rear.put("supportedPictureSizes", rearSizesJs);
|
|
600
|
+
supportedPictureSizesResult.put(rear);
|
|
601
|
+
|
|
602
|
+
List<Size> frontSizes = CameraXView.getSupportedPictureSizes("front");
|
|
603
|
+
JSObject front = new JSObject();
|
|
604
|
+
front.put("facing", "front");
|
|
605
|
+
JSArray frontSizesJs = new JSArray();
|
|
606
|
+
for (Size size : frontSizes) {
|
|
607
|
+
JSObject sizeJs = new JSObject();
|
|
608
|
+
sizeJs.put("width", size.getWidth());
|
|
609
|
+
sizeJs.put("height", size.getHeight());
|
|
610
|
+
frontSizesJs.put(sizeJs);
|
|
611
|
+
}
|
|
612
|
+
front.put("supportedPictureSizes", frontSizesJs);
|
|
613
|
+
supportedPictureSizesResult.put(front);
|
|
1218
614
|
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
615
|
+
JSObject ret = new JSObject();
|
|
616
|
+
ret.put("supportedPictureSizes", supportedPictureSizesResult);
|
|
617
|
+
call.resolve(ret);
|
|
618
|
+
}
|
|
1222
619
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
620
|
+
@PluginMethod
|
|
621
|
+
public void setOpacity(PluginCall call) {
|
|
622
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
623
|
+
call.reject("Camera is not running");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
Float opacity = call.getFloat("opacity", 1.0f);
|
|
627
|
+
//noinspection DataFlowIssue
|
|
628
|
+
cameraXView.setOpacity(opacity);
|
|
629
|
+
call.resolve();
|
|
630
|
+
}
|
|
1231
631
|
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
)
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
);
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
);
|
|
632
|
+
@PluginMethod
|
|
633
|
+
public void getHorizontalFov(PluginCall call) {
|
|
634
|
+
// CameraX does not provide a simple way to get FoV.
|
|
635
|
+
// This would require Camera2 interop to access camera characteristics.
|
|
636
|
+
// Returning a default/estimated value.
|
|
637
|
+
JSObject ret = new JSObject();
|
|
638
|
+
ret.put("result", 60.0); // A common default FoV
|
|
639
|
+
call.resolve(ret);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
@PluginMethod
|
|
643
|
+
public void getDeviceId(PluginCall call) {
|
|
644
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
645
|
+
call.reject("Camera is not running");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
JSObject ret = new JSObject();
|
|
649
|
+
ret.put("deviceId", cameraXView.getCurrentDeviceId());
|
|
650
|
+
call.resolve(ret);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
@PluginMethod
|
|
654
|
+
public void getFlashMode(PluginCall call) {
|
|
655
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
656
|
+
call.reject("Camera is not running");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
JSObject ret = new JSObject();
|
|
660
|
+
ret.put("flashMode", cameraXView.getFlashMode());
|
|
661
|
+
call.resolve(ret);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
@PluginMethod
|
|
665
|
+
public void isRunning(PluginCall call) {
|
|
666
|
+
boolean running = cameraXView != null && cameraXView.isRunning();
|
|
667
|
+
JSObject jsObject = new JSObject();
|
|
668
|
+
jsObject.put("isRunning", running);
|
|
669
|
+
call.resolve(jsObject);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private void showCameraPermissionDialog(String title, String message, String openSettingsText, String cancelText, Runnable completion) {
|
|
673
|
+
Activity activity = getActivity();
|
|
674
|
+
if (activity == null) {
|
|
675
|
+
if (completion != null) {
|
|
676
|
+
completion.run();
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
activity.runOnUiThread(() -> {
|
|
682
|
+
if (activity.isFinishing()) {
|
|
683
|
+
if (completion != null) {
|
|
684
|
+
completion.run();
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (isCameraPermissionDialogShowing) {
|
|
690
|
+
if (completion != null) {
|
|
691
|
+
completion.run();
|
|
692
|
+
}
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
AlertDialog dialog = new AlertDialog.Builder(activity)
|
|
697
|
+
.setTitle(title)
|
|
698
|
+
.setMessage(message)
|
|
699
|
+
.setNegativeButton(cancelText, (d, which) -> {
|
|
700
|
+
d.dismiss();
|
|
701
|
+
isCameraPermissionDialogShowing = false;
|
|
702
|
+
})
|
|
703
|
+
.setPositiveButton(openSettingsText, (d, which) -> {
|
|
704
|
+
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
|
|
705
|
+
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
|
|
706
|
+
intent.setData(uri);
|
|
707
|
+
activity.startActivity(intent);
|
|
708
|
+
isCameraPermissionDialogShowing = false;
|
|
709
|
+
})
|
|
710
|
+
.setOnDismissListener(d -> isCameraPermissionDialogShowing = false)
|
|
711
|
+
.create();
|
|
712
|
+
|
|
713
|
+
isCameraPermissionDialogShowing = true;
|
|
714
|
+
dialog.show();
|
|
715
|
+
if (completion != null) {
|
|
716
|
+
completion.run();
|
|
717
|
+
}
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private String mapPermissionState(PermissionState state) {
|
|
722
|
+
if (state == null) {
|
|
723
|
+
return PermissionState.PROMPT.toString();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return state.toString();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
@PluginMethod
|
|
730
|
+
public void checkPermissions(PluginCall call) {
|
|
731
|
+
boolean disableAudio = call.getBoolean("disableAudio") != null ? Boolean.TRUE.equals(call.getBoolean("disableAudio")) : true;
|
|
732
|
+
|
|
733
|
+
PermissionState cameraState = getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS);
|
|
734
|
+
|
|
735
|
+
JSObject result = new JSObject();
|
|
736
|
+
result.put("camera", mapPermissionState(cameraState));
|
|
737
|
+
|
|
738
|
+
if (!disableAudio) {
|
|
739
|
+
PermissionState audioState = getPermissionState(MICROPHONE_ONLY_PERMISSION_ALIAS);
|
|
740
|
+
result.put("microphone", mapPermissionState(audioState));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
call.resolve(result);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
@Override
|
|
747
|
+
@PluginMethod
|
|
748
|
+
public void requestPermissions(PluginCall call) {
|
|
749
|
+
Boolean disableAudioOption = call.getBoolean("disableAudio");
|
|
750
|
+
boolean disableAudio = disableAudioOption == null ? true : Boolean.TRUE.equals(disableAudioOption);
|
|
751
|
+
this.lastDisableAudio = disableAudio;
|
|
752
|
+
|
|
753
|
+
String permissionAlias = disableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
|
|
754
|
+
|
|
755
|
+
boolean cameraGranted = PermissionState.GRANTED.equals(getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS));
|
|
756
|
+
boolean audioGranted = disableAudio || PermissionState.GRANTED.equals(getPermissionState(MICROPHONE_ONLY_PERMISSION_ALIAS));
|
|
757
|
+
|
|
758
|
+
if (cameraGranted && audioGranted) {
|
|
759
|
+
JSObject result = new JSObject();
|
|
760
|
+
result.put("camera", mapPermissionState(PermissionState.GRANTED));
|
|
761
|
+
if (!disableAudio) {
|
|
762
|
+
result.put("microphone", mapPermissionState(PermissionState.GRANTED));
|
|
763
|
+
}
|
|
764
|
+
call.resolve(result);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
requestPermissionForAlias(permissionAlias, call, "handleRequestPermissionsResult");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
@PermissionCallback
|
|
772
|
+
private void handleRequestPermissionsResult(PluginCall call) {
|
|
773
|
+
Boolean disableAudioOption = call.getBoolean("disableAudio");
|
|
774
|
+
boolean disableAudio = disableAudioOption == null ? true : Boolean.TRUE.equals(disableAudioOption);
|
|
775
|
+
this.lastDisableAudio = disableAudio;
|
|
776
|
+
|
|
777
|
+
PermissionState cameraState = getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS);
|
|
778
|
+
JSObject result = new JSObject();
|
|
779
|
+
result.put("camera", mapPermissionState(cameraState));
|
|
780
|
+
|
|
781
|
+
if (!disableAudio) {
|
|
782
|
+
PermissionState audioState = getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS);
|
|
783
|
+
result.put("microphone", mapPermissionState(audioState));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
boolean showSettingsAlert = call.getBoolean("showSettingsAlert") != null
|
|
787
|
+
? Boolean.TRUE.equals(call.getBoolean("showSettingsAlert"))
|
|
788
|
+
: false;
|
|
789
|
+
|
|
790
|
+
String cameraStateString = result.getString("camera");
|
|
791
|
+
boolean cameraNeedsSettings = "denied".equals(cameraStateString) || "prompt-with-rationale".equals(cameraStateString);
|
|
792
|
+
|
|
793
|
+
boolean microphoneNeedsSettings = false;
|
|
794
|
+
if (result.has("microphone")) {
|
|
795
|
+
String micStateString = result.getString("microphone");
|
|
796
|
+
microphoneNeedsSettings = "denied".equals(micStateString) || "prompt-with-rationale".equals(micStateString);
|
|
797
|
+
}
|
|
1267
798
|
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
",
|
|
1279
|
-
|
|
1280
|
-
|
|
799
|
+
boolean shouldShowAlert = showSettingsAlert && (cameraNeedsSettings || microphoneNeedsSettings);
|
|
800
|
+
|
|
801
|
+
if (shouldShowAlert) {
|
|
802
|
+
Activity activity = getActivity();
|
|
803
|
+
if (activity == null) {
|
|
804
|
+
call.resolve(result);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
String title = call.getString("title", "Camera Permission Needed");
|
|
809
|
+
String message = call.getString("message", "Enable camera access in Settings to use the preview.");
|
|
810
|
+
String openSettingsText = call.getString("openSettingsButtonTitle", "Open Settings");
|
|
811
|
+
String cancelText = call.getString("cancelButtonTitle", activity.getString(android.R.string.cancel));
|
|
812
|
+
|
|
813
|
+
showCameraPermissionDialog(title, message, openSettingsText, cancelText, () -> call.resolve(result));
|
|
1281
814
|
} else {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
"
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
815
|
+
call.resolve(result);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
@PermissionCallback
|
|
820
|
+
private void handleCameraPermissionResult(PluginCall call) {
|
|
821
|
+
if (
|
|
822
|
+
PermissionState.GRANTED.equals(getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)) ||
|
|
823
|
+
PermissionState.GRANTED.equals(getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS))
|
|
824
|
+
) {
|
|
825
|
+
startCamera(call);
|
|
826
|
+
} else {
|
|
827
|
+
call.reject("camera permission denied. enable camera access in Settings.", "cameraPermissionDenied");
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private void startCamera(final PluginCall call) {
|
|
832
|
+
String positionParam = call.getString("position");
|
|
833
|
+
String originalDeviceId = call.getString("deviceId");
|
|
834
|
+
String deviceId = originalDeviceId; // Use a mutable variable
|
|
835
|
+
|
|
836
|
+
final String position = (positionParam == null ||
|
|
837
|
+
positionParam.isEmpty() ||
|
|
838
|
+
"rear".equals(positionParam) ||
|
|
839
|
+
"back".equals(positionParam))
|
|
840
|
+
? "back"
|
|
841
|
+
: "front";
|
|
842
|
+
// Use -1 as default to indicate centering is needed when x/y not provided
|
|
843
|
+
final Integer xParam = call.getInt("x");
|
|
844
|
+
final Integer yParam = call.getInt("y");
|
|
845
|
+
final int x = xParam != null ? xParam : -1;
|
|
846
|
+
final int y = yParam != null ? yParam : -1;
|
|
847
|
+
|
|
848
|
+
Log.d("CameraPreview", "========================");
|
|
849
|
+
Log.d("CameraPreview", "CAMERA POSITION TRACKING START:");
|
|
850
|
+
Log.d("CameraPreview", "1. RAW PARAMS - xParam: " + xParam + ", yParam: " + yParam);
|
|
851
|
+
Log.d("CameraPreview", "2. AFTER DEFAULT - x: " + x + " (center=" + (x == -1) + "), y: " + y + " (center=" + (y == -1) + ")");
|
|
852
|
+
//noinspection DataFlowIssue
|
|
853
|
+
final int width = call.getInt("width", 0);
|
|
854
|
+
//noinspection DataFlowIssue
|
|
855
|
+
final int height = call.getInt("height", 0);
|
|
856
|
+
//noinspection DataFlowIssue
|
|
857
|
+
final int paddingBottom = call.getInt("paddingBottom", 0);
|
|
858
|
+
final boolean toBack = Boolean.TRUE.equals(call.getBoolean("toBack", true));
|
|
859
|
+
final boolean storeToFile = Boolean.TRUE.equals(call.getBoolean("storeToFile", false));
|
|
860
|
+
final boolean enableOpacity = Boolean.TRUE.equals(call.getBoolean("enableOpacity", false));
|
|
861
|
+
final boolean disableExifHeaderStripping = Boolean.TRUE.equals(call.getBoolean("disableExifHeaderStripping", false));
|
|
862
|
+
final boolean lockOrientation = Boolean.TRUE.equals(call.getBoolean("lockAndroidOrientation", false));
|
|
863
|
+
final boolean disableAudio = Boolean.TRUE.equals(call.getBoolean("disableAudio", true));
|
|
864
|
+
this.lastDisableAudio = disableAudio;
|
|
865
|
+
final String aspectRatio = call.getString("aspectRatio", "4:3");
|
|
866
|
+
final String gridMode = call.getString("gridMode", "none");
|
|
867
|
+
final String positioning = call.getString("positioning", "top");
|
|
868
|
+
//noinspection DataFlowIssue
|
|
869
|
+
final float initialZoomLevel = call.getFloat("initialZoomLevel", 1.0f);
|
|
870
|
+
//noinspection DataFlowIssue
|
|
871
|
+
final boolean disableFocusIndicator = call.getBoolean("disableFocusIndicator", false);
|
|
872
|
+
final boolean enableVideoMode = Boolean.TRUE.equals(call.getBoolean("enableVideoMode", false));
|
|
873
|
+
|
|
874
|
+
// Check for conflict between aspectRatio and size
|
|
875
|
+
if (call.getData().has("aspectRatio") && (call.getData().has("width") || call.getData().has("height"))) {
|
|
876
|
+
call.reject("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.");
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
float targetZoom = initialZoomLevel;
|
|
881
|
+
// Check if the selected device is a physical ultra-wide
|
|
882
|
+
if (originalDeviceId != null) {
|
|
883
|
+
List<CameraDevice> devices = CameraXView.getAvailableDevicesStatic(getContext());
|
|
884
|
+
for (CameraDevice device : devices) {
|
|
885
|
+
if (originalDeviceId.equals(device.getDeviceId()) && !device.isLogical()) {
|
|
886
|
+
for (LensInfo lens : device.getLenses()) {
|
|
887
|
+
if ("ultraWide".equals(lens.getDeviceType())) {
|
|
888
|
+
Log.d("CameraPreview", "Ultra-wide lens selected. Targeting 0.5x zoom on logical camera.");
|
|
889
|
+
targetZoom = 0.5f;
|
|
890
|
+
// Force the use of the logical camera by clearing the specific deviceId
|
|
891
|
+
deviceId = null;
|
|
892
|
+
break;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (deviceId == null) break; // Exit outer loop once we've made our decision
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
previousOrientationRequest = getBridge().getActivity().getRequestedOrientation();
|
|
901
|
+
cameraXView = new CameraXView(getContext(), getBridge().getWebView());
|
|
902
|
+
cameraXView.setListener(this);
|
|
903
|
+
|
|
904
|
+
String finalDeviceId = deviceId;
|
|
905
|
+
float finalTargetZoom = targetZoom;
|
|
906
|
+
getBridge()
|
|
907
|
+
.getActivity()
|
|
908
|
+
.runOnUiThread(() -> {
|
|
909
|
+
// Ensure transparent background when preview is behind the WebView (Android 10 fix)
|
|
910
|
+
if (toBack) {
|
|
911
|
+
try {
|
|
912
|
+
if (originalWindowBackground == null) {
|
|
913
|
+
originalWindowBackground = getBridge().getActivity().getWindow().getDecorView().getBackground();
|
|
914
|
+
}
|
|
915
|
+
getBridge().getActivity().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
|
916
|
+
} catch (Exception ignored) {}
|
|
917
|
+
}
|
|
918
|
+
DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
|
|
919
|
+
if (lockOrientation) {
|
|
920
|
+
getBridge().getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Debug: Let's check all the positioning information
|
|
924
|
+
ViewGroup webViewParent = (ViewGroup) getBridge().getWebView().getParent();
|
|
925
|
+
|
|
926
|
+
// Get webview position in different coordinate systems
|
|
927
|
+
int[] webViewLocationInWindow = new int[2];
|
|
928
|
+
int[] webViewLocationOnScreen = new int[2];
|
|
929
|
+
getBridge().getWebView().getLocationInWindow(webViewLocationInWindow);
|
|
930
|
+
getBridge().getWebView().getLocationOnScreen(webViewLocationOnScreen);
|
|
931
|
+
|
|
932
|
+
int webViewLeft = getBridge().getWebView().getLeft();
|
|
933
|
+
int webViewTop = getBridge().getWebView().getTop();
|
|
934
|
+
|
|
935
|
+
// Check parent position too
|
|
936
|
+
int[] parentLocationInWindow = new int[2];
|
|
937
|
+
int[] parentLocationOnScreen = new int[2];
|
|
938
|
+
webViewParent.getLocationInWindow(parentLocationInWindow);
|
|
939
|
+
webViewParent.getLocationOnScreen(parentLocationOnScreen);
|
|
940
|
+
|
|
941
|
+
// Calculate pixel ratio
|
|
942
|
+
float pixelRatio = metrics.density;
|
|
943
|
+
|
|
944
|
+
// The key insight: JavaScript coordinates are relative to the WebView's viewport
|
|
945
|
+
// If the WebView is positioned below the status bar (webViewLocationOnScreen[1] > 0),
|
|
946
|
+
// we need to add that offset when placing native views
|
|
947
|
+
int webViewTopInset = webViewLocationOnScreen[1];
|
|
948
|
+
boolean isEdgeToEdgeActive = webViewLocationOnScreen[1] > 0;
|
|
949
|
+
|
|
950
|
+
// Log all the positioning information for debugging
|
|
951
|
+
Log.d("CameraPreview", "WebView Position Debug:");
|
|
952
|
+
Log.d("CameraPreview", " - webView.getTop(): " + webViewTop);
|
|
953
|
+
Log.d("CameraPreview", " - webView.getLeft(): " + webViewLeft);
|
|
1322
954
|
Log.d(
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
screenHeight +
|
|
1326
|
-
", webViewTop=" +
|
|
1327
|
-
webViewTopInset +
|
|
1328
|
-
", computedHeight=" +
|
|
1329
|
-
computedHeight +
|
|
1330
|
-
", computedY=" +
|
|
1331
|
-
computedY
|
|
955
|
+
"CameraPreview",
|
|
956
|
+
" - webView locationInWindow: (" + webViewLocationInWindow[0] + ", " + webViewLocationInWindow[1] + ")"
|
|
1332
957
|
);
|
|
1333
|
-
} else {
|
|
1334
|
-
// Normal mode - use full screen height
|
|
1335
|
-
computedY = (screenHeight - computedHeight) / 2;
|
|
1336
958
|
Log.d(
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
959
|
+
"CameraPreview",
|
|
960
|
+
" - webView locationOnScreen: (" + webViewLocationOnScreen[0] + ", " + webViewLocationOnScreen[1] + ")"
|
|
961
|
+
);
|
|
962
|
+
Log.d(
|
|
963
|
+
"CameraPreview",
|
|
964
|
+
" - parent locationInWindow: (" + parentLocationInWindow[0] + ", " + parentLocationInWindow[1] + ")"
|
|
965
|
+
);
|
|
966
|
+
Log.d(
|
|
967
|
+
"CameraPreview",
|
|
968
|
+
" - parent locationOnScreen: (" + parentLocationOnScreen[0] + ", " + parentLocationOnScreen[1] + ")"
|
|
1344
969
|
);
|
|
1345
|
-
}
|
|
1346
|
-
break;
|
|
1347
|
-
}
|
|
1348
|
-
} else {
|
|
1349
|
-
computedY = (int) (y * pixelRatio);
|
|
1350
|
-
// If edge-to-edge is active, JavaScript Y is relative to WebView content area
|
|
1351
|
-
// We need to add the inset to get absolute screen position
|
|
1352
|
-
if (isEdgeToEdgeActive) {
|
|
1353
|
-
computedY += webViewTopInset;
|
|
1354
|
-
Log.d(
|
|
1355
|
-
"CameraPreview",
|
|
1356
|
-
"Edge-to-edge adjustment: Y position " +
|
|
1357
|
-
(int) (y * pixelRatio) +
|
|
1358
|
-
" + inset " +
|
|
1359
|
-
webViewTopInset +
|
|
1360
|
-
" = " +
|
|
1361
|
-
computedY
|
|
1362
|
-
);
|
|
1363
|
-
}
|
|
1364
|
-
Log.d(
|
|
1365
|
-
"CameraPreview",
|
|
1366
|
-
"Using provided Y position: " +
|
|
1367
|
-
y +
|
|
1368
|
-
" * " +
|
|
1369
|
-
pixelRatio +
|
|
1370
|
-
" = " +
|
|
1371
|
-
computedY +
|
|
1372
|
-
(isEdgeToEdgeActive ? " (adjusted for edge-to-edge)" : "")
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
970
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
);
|
|
1394
|
-
Log.d("CameraPreview", "=== COORDINATE DEBUG ===");
|
|
1395
|
-
Log.d(
|
|
1396
|
-
"CameraPreview",
|
|
1397
|
-
"WebView getLeft/getTop: (" + webViewLeft + ", " + webViewTop + ")"
|
|
1398
|
-
);
|
|
1399
|
-
Log.d(
|
|
1400
|
-
"CameraPreview",
|
|
1401
|
-
"WebView locationInWindow: (" +
|
|
1402
|
-
webViewLocationInWindow[0] +
|
|
1403
|
-
", " +
|
|
1404
|
-
webViewLocationInWindow[1] +
|
|
1405
|
-
")"
|
|
1406
|
-
);
|
|
1407
|
-
Log.d(
|
|
1408
|
-
"CameraPreview",
|
|
1409
|
-
"WebView locationOnScreen: (" +
|
|
1410
|
-
webViewLocationOnScreen[0] +
|
|
1411
|
-
", " +
|
|
1412
|
-
webViewLocationOnScreen[1] +
|
|
1413
|
-
")"
|
|
1414
|
-
);
|
|
1415
|
-
Log.d(
|
|
1416
|
-
"CameraPreview",
|
|
1417
|
-
"Parent locationInWindow: (" +
|
|
1418
|
-
parentLocationInWindow[0] +
|
|
1419
|
-
", " +
|
|
1420
|
-
parentLocationInWindow[1] +
|
|
1421
|
-
")"
|
|
1422
|
-
);
|
|
1423
|
-
Log.d(
|
|
1424
|
-
"CameraPreview",
|
|
1425
|
-
"Parent locationOnScreen: (" +
|
|
1426
|
-
parentLocationOnScreen[0] +
|
|
1427
|
-
", " +
|
|
1428
|
-
parentLocationOnScreen[1] +
|
|
1429
|
-
")"
|
|
1430
|
-
);
|
|
1431
|
-
Log.d(
|
|
1432
|
-
"CameraPreview",
|
|
1433
|
-
"Parent class: " + webViewParent.getClass().getSimpleName()
|
|
1434
|
-
);
|
|
1435
|
-
Log.d(
|
|
1436
|
-
"CameraPreview",
|
|
1437
|
-
"Requested position (logical): (" + x + ", " + y + ")"
|
|
1438
|
-
);
|
|
1439
|
-
Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
|
|
1440
|
-
Log.d(
|
|
1441
|
-
"CameraPreview",
|
|
1442
|
-
"Final computed position (no offset): (" +
|
|
1443
|
-
computedX +
|
|
1444
|
-
", " +
|
|
1445
|
-
computedY +
|
|
1446
|
-
")"
|
|
1447
|
-
);
|
|
1448
|
-
Log.d("CameraPreview", "5. IS_CENTERED - " + (x == -1 || y == -1));
|
|
1449
|
-
Log.d("CameraPreview", "========================");
|
|
971
|
+
// Check if WebView has margins
|
|
972
|
+
View webView = getBridge().getWebView();
|
|
973
|
+
ViewGroup.LayoutParams webViewLayoutParams = webView.getLayoutParams();
|
|
974
|
+
if (webViewLayoutParams instanceof ViewGroup.MarginLayoutParams) {
|
|
975
|
+
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) webViewLayoutParams;
|
|
976
|
+
Log.d(
|
|
977
|
+
"CameraPreview",
|
|
978
|
+
" - webView margins: left=" +
|
|
979
|
+
marginParams.leftMargin +
|
|
980
|
+
", top=" +
|
|
981
|
+
marginParams.topMargin +
|
|
982
|
+
", right=" +
|
|
983
|
+
marginParams.rightMargin +
|
|
984
|
+
", bottom=" +
|
|
985
|
+
marginParams.bottomMargin
|
|
986
|
+
);
|
|
987
|
+
}
|
|
1450
988
|
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
storeToFile,
|
|
1464
|
-
enableOpacity,
|
|
1465
|
-
disableExifHeaderStripping,
|
|
1466
|
-
disableAudio,
|
|
1467
|
-
1.0f,
|
|
1468
|
-
aspectRatio,
|
|
1469
|
-
gridMode,
|
|
1470
|
-
disableFocusIndicator,
|
|
1471
|
-
enableVideoMode
|
|
1472
|
-
);
|
|
1473
|
-
config.setTargetZoom(finalTargetZoom);
|
|
1474
|
-
config.setCentered(isCentered);
|
|
989
|
+
// Check WebView padding
|
|
990
|
+
Log.d(
|
|
991
|
+
"CameraPreview",
|
|
992
|
+
" - webView padding: left=" +
|
|
993
|
+
webView.getPaddingLeft() +
|
|
994
|
+
", top=" +
|
|
995
|
+
webView.getPaddingTop() +
|
|
996
|
+
", right=" +
|
|
997
|
+
webView.getPaddingRight() +
|
|
998
|
+
", bottom=" +
|
|
999
|
+
webView.getPaddingBottom()
|
|
1000
|
+
);
|
|
1475
1001
|
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
cameraXView.startSession(config);
|
|
1479
|
-
|
|
1480
|
-
// Setup orientation listener to mirror iOS screenResize emission
|
|
1481
|
-
if (orientationListener == null) {
|
|
1482
|
-
lastOrientation = getContext()
|
|
1483
|
-
.getResources()
|
|
1484
|
-
.getConfiguration()
|
|
1485
|
-
.orientation;
|
|
1486
|
-
lastOrientationStr = getDeviceOrientationString();
|
|
1487
|
-
orientationListener = new OrientationEventListener(getContext()) {
|
|
1488
|
-
@Override
|
|
1489
|
-
public void onOrientationChanged(int orientation) {
|
|
1490
|
-
if (orientation == ORIENTATION_UNKNOWN) return;
|
|
1491
|
-
int current = getContext()
|
|
1492
|
-
.getResources()
|
|
1493
|
-
.getConfiguration()
|
|
1494
|
-
.orientation;
|
|
1495
|
-
String currentStr = getDeviceOrientationString();
|
|
1496
|
-
if (
|
|
1497
|
-
current != lastOrientation ||
|
|
1498
|
-
!Objects.equals(currentStr, lastOrientationStr)
|
|
1499
|
-
) {
|
|
1500
|
-
lastOrientation = current;
|
|
1501
|
-
lastOrientationStr = currentStr;
|
|
1502
|
-
// Post to next frame so WebView has updated bounds before we recompute layout
|
|
1503
|
-
getBridge()
|
|
1504
|
-
.getActivity()
|
|
1505
|
-
.getWindow()
|
|
1506
|
-
.getDecorView()
|
|
1507
|
-
.post(() -> handleOrientationChange());
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
};
|
|
1511
|
-
if (orientationListener.canDetectOrientation()) {
|
|
1512
|
-
orientationListener.enable();
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
});
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
private void handleOrientationChange() {
|
|
1519
|
-
if (cameraXView == null || !cameraXView.isRunning()) return;
|
|
1520
|
-
|
|
1521
|
-
Log.d(
|
|
1522
|
-
TAG,
|
|
1523
|
-
"======================== ORIENTATION CHANGE DETECTED ========================"
|
|
1524
|
-
);
|
|
1525
|
-
|
|
1526
|
-
// Get comprehensive display and orientation information
|
|
1527
|
-
android.util.DisplayMetrics metrics = getContext()
|
|
1528
|
-
.getResources()
|
|
1529
|
-
.getDisplayMetrics();
|
|
1530
|
-
int screenWidthPx = metrics.widthPixels;
|
|
1531
|
-
int screenHeightPx = metrics.heightPixels;
|
|
1532
|
-
float density = metrics.density;
|
|
1533
|
-
int screenWidthDp = (int) (screenWidthPx / density);
|
|
1534
|
-
int screenHeightDp = (int) (screenHeightPx / density);
|
|
1535
|
-
|
|
1536
|
-
int current = getContext().getResources().getConfiguration().orientation;
|
|
1537
|
-
Log.d(TAG, "New orientation: " + current + " (1=PORTRAIT, 2=LANDSCAPE)");
|
|
1538
|
-
Log.d(
|
|
1539
|
-
TAG,
|
|
1540
|
-
"Screen dimensions - Pixels: " +
|
|
1541
|
-
screenWidthPx +
|
|
1542
|
-
"x" +
|
|
1543
|
-
screenHeightPx +
|
|
1544
|
-
", DP: " +
|
|
1545
|
-
screenWidthDp +
|
|
1546
|
-
"x" +
|
|
1547
|
-
screenHeightDp +
|
|
1548
|
-
", Density: " +
|
|
1549
|
-
density
|
|
1550
|
-
);
|
|
1551
|
-
|
|
1552
|
-
// Get WebView dimensions before rotation
|
|
1553
|
-
WebView webView = getBridge().getWebView();
|
|
1554
|
-
int webViewWidth = webView.getWidth();
|
|
1555
|
-
int webViewHeight = webView.getHeight();
|
|
1556
|
-
Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
|
|
1557
|
-
|
|
1558
|
-
// Get current preview bounds before rotation
|
|
1559
|
-
int[] oldBounds = cameraXView.getCurrentPreviewBounds();
|
|
1560
|
-
Log.d(
|
|
1561
|
-
TAG,
|
|
1562
|
-
"Current preview bounds before rotation: x=" +
|
|
1563
|
-
oldBounds[0] +
|
|
1564
|
-
", y=" +
|
|
1565
|
-
oldBounds[1] +
|
|
1566
|
-
", width=" +
|
|
1567
|
-
oldBounds[2] +
|
|
1568
|
-
", height=" +
|
|
1569
|
-
oldBounds[3]
|
|
1570
|
-
);
|
|
1571
|
-
|
|
1572
|
-
getBridge()
|
|
1573
|
-
.getActivity()
|
|
1574
|
-
.runOnUiThread(() -> {
|
|
1575
|
-
// Create and show a black full-screen overlay during rotation
|
|
1576
|
-
ViewGroup rootView = (ViewGroup) getBridge()
|
|
1577
|
-
.getActivity()
|
|
1578
|
-
.getWindow()
|
|
1579
|
-
.getDecorView()
|
|
1580
|
-
.getRootView();
|
|
1581
|
-
|
|
1582
|
-
// Remove any existing overlay
|
|
1583
|
-
if (rotationOverlay != null && rotationOverlay.getParent() != null) {
|
|
1584
|
-
((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
// Create new black overlay
|
|
1588
|
-
rotationOverlay = new View(getContext());
|
|
1589
|
-
rotationOverlay.setBackgroundColor(Color.BLACK);
|
|
1590
|
-
ViewGroup.LayoutParams overlayParams = new ViewGroup.LayoutParams(
|
|
1591
|
-
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
1592
|
-
ViewGroup.LayoutParams.MATCH_PARENT
|
|
1593
|
-
);
|
|
1594
|
-
rotationOverlay.setLayoutParams(overlayParams);
|
|
1595
|
-
rootView.addView(rotationOverlay);
|
|
1596
|
-
|
|
1597
|
-
// Reapply current aspect ratio to recompute layout, then emit screenResize
|
|
1598
|
-
String ar = cameraXView.getAspectRatio();
|
|
1599
|
-
Log.d(TAG, "Reapplying aspect ratio: " + ar);
|
|
1600
|
-
|
|
1601
|
-
// Re-get dimensions after potential layout pass
|
|
1602
|
-
android.util.DisplayMetrics newMetrics = getContext()
|
|
1603
|
-
.getResources()
|
|
1604
|
-
.getDisplayMetrics();
|
|
1605
|
-
int newScreenWidthPx = newMetrics.widthPixels;
|
|
1606
|
-
int newScreenHeightPx = newMetrics.heightPixels;
|
|
1607
|
-
int newWebViewWidth = webView.getWidth();
|
|
1608
|
-
int newWebViewHeight = webView.getHeight();
|
|
1002
|
+
Log.d("CameraPreview", " - Using webViewTopInset: " + webViewTopInset);
|
|
1003
|
+
Log.d("CameraPreview", " - isEdgeToEdgeActive: " + isEdgeToEdgeActive);
|
|
1609
1004
|
|
|
1005
|
+
// Calculate position - center if x or y is -1
|
|
1006
|
+
int computedX;
|
|
1007
|
+
int computedY;
|
|
1008
|
+
|
|
1009
|
+
// Calculate dimensions first
|
|
1010
|
+
int computedWidth = width != 0 ? (int) (width * pixelRatio) : getBridge().getWebView().getWidth();
|
|
1011
|
+
int computedHeight = height != 0 ? (int) (height * pixelRatio) : getBridge().getWebView().getHeight();
|
|
1012
|
+
computedHeight -= (int) (paddingBottom * pixelRatio);
|
|
1013
|
+
|
|
1014
|
+
Log.d("CameraPreview", "========================");
|
|
1015
|
+
Log.d("CameraPreview", "POSITIONING CALCULATIONS:");
|
|
1016
|
+
Log.d("CameraPreview", "1. INPUT - x: " + x + ", y: " + y + ", width: " + width + ", height: " + height);
|
|
1017
|
+
Log.d("CameraPreview", "2. PIXEL RATIO: " + pixelRatio);
|
|
1018
|
+
Log.d("CameraPreview", "3. SCREEN - width: " + metrics.widthPixels + ", height: " + metrics.heightPixels);
|
|
1019
|
+
Log.d(
|
|
1020
|
+
"CameraPreview",
|
|
1021
|
+
"4. WEBVIEW - width: " + getBridge().getWebView().getWidth() + ", height: " + getBridge().getWebView().getHeight()
|
|
1022
|
+
);
|
|
1023
|
+
Log.d("CameraPreview", "5. COMPUTED DIMENSIONS - width: " + computedWidth + ", height: " + computedHeight);
|
|
1024
|
+
|
|
1025
|
+
if (x == -1) {
|
|
1026
|
+
// Center horizontally
|
|
1027
|
+
int screenWidth = metrics.widthPixels;
|
|
1028
|
+
computedX = (screenWidth - computedWidth) / 2;
|
|
1029
|
+
Log.d(
|
|
1030
|
+
"CameraPreview",
|
|
1031
|
+
"Centering horizontally: screenWidth=" +
|
|
1032
|
+
screenWidth +
|
|
1033
|
+
", computedWidth=" +
|
|
1034
|
+
computedWidth +
|
|
1035
|
+
", computedX=" +
|
|
1036
|
+
computedX
|
|
1037
|
+
);
|
|
1038
|
+
} else {
|
|
1039
|
+
computedX = (int) (x * pixelRatio);
|
|
1040
|
+
Log.d("CameraPreview", "Using provided X position: " + x + " * " + pixelRatio + " = " + computedX);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (y == -1) {
|
|
1044
|
+
// Position vertically based on positioning parameter
|
|
1045
|
+
int screenHeight = metrics.heightPixels;
|
|
1046
|
+
|
|
1047
|
+
switch (Objects.requireNonNull(positioning)) {
|
|
1048
|
+
case "top":
|
|
1049
|
+
computedY = 0;
|
|
1050
|
+
Log.d("CameraPreview", "Positioning at top: computedY=0");
|
|
1051
|
+
break;
|
|
1052
|
+
case "bottom":
|
|
1053
|
+
computedY = screenHeight - computedHeight;
|
|
1054
|
+
Log.d(
|
|
1055
|
+
"CameraPreview",
|
|
1056
|
+
"Positioning at bottom: screenHeight=" +
|
|
1057
|
+
screenHeight +
|
|
1058
|
+
", computedHeight=" +
|
|
1059
|
+
computedHeight +
|
|
1060
|
+
", computedY=" +
|
|
1061
|
+
computedY
|
|
1062
|
+
);
|
|
1063
|
+
break;
|
|
1064
|
+
case "center":
|
|
1065
|
+
default:
|
|
1066
|
+
// Center vertically
|
|
1067
|
+
if (isEdgeToEdgeActive) {
|
|
1068
|
+
// When WebView is offset from top, center within the available space
|
|
1069
|
+
// The camera should be centered in the full screen, not just the WebView area
|
|
1070
|
+
computedY = (screenHeight - computedHeight) / 2;
|
|
1071
|
+
Log.d(
|
|
1072
|
+
"CameraPreview",
|
|
1073
|
+
"Centering vertically with WebView offset: screenHeight=" +
|
|
1074
|
+
screenHeight +
|
|
1075
|
+
", webViewTop=" +
|
|
1076
|
+
webViewTopInset +
|
|
1077
|
+
", computedHeight=" +
|
|
1078
|
+
computedHeight +
|
|
1079
|
+
", computedY=" +
|
|
1080
|
+
computedY
|
|
1081
|
+
);
|
|
1082
|
+
} else {
|
|
1083
|
+
// Normal mode - use full screen height
|
|
1084
|
+
computedY = (screenHeight - computedHeight) / 2;
|
|
1085
|
+
Log.d(
|
|
1086
|
+
"CameraPreview",
|
|
1087
|
+
"Centering vertically (normal): screenHeight=" +
|
|
1088
|
+
screenHeight +
|
|
1089
|
+
", computedHeight=" +
|
|
1090
|
+
computedHeight +
|
|
1091
|
+
", computedY=" +
|
|
1092
|
+
computedY
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
break;
|
|
1096
|
+
}
|
|
1097
|
+
} else {
|
|
1098
|
+
computedY = (int) (y * pixelRatio);
|
|
1099
|
+
// If edge-to-edge is active, JavaScript Y is relative to WebView content area
|
|
1100
|
+
// We need to add the inset to get absolute screen position
|
|
1101
|
+
if (isEdgeToEdgeActive) {
|
|
1102
|
+
computedY += webViewTopInset;
|
|
1103
|
+
Log.d(
|
|
1104
|
+
"CameraPreview",
|
|
1105
|
+
"Edge-to-edge adjustment: Y position " +
|
|
1106
|
+
(int) (y * pixelRatio) +
|
|
1107
|
+
" + inset " +
|
|
1108
|
+
webViewTopInset +
|
|
1109
|
+
" = " +
|
|
1110
|
+
computedY
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
Log.d(
|
|
1114
|
+
"CameraPreview",
|
|
1115
|
+
"Using provided Y position: " +
|
|
1116
|
+
y +
|
|
1117
|
+
" * " +
|
|
1118
|
+
pixelRatio +
|
|
1119
|
+
" = " +
|
|
1120
|
+
computedY +
|
|
1121
|
+
(isEdgeToEdgeActive ? " (adjusted for edge-to-edge)" : "")
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
Log.d(
|
|
1126
|
+
"CameraPreview",
|
|
1127
|
+
"2b. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE (inset=" + webViewTopInset + ")" : "INACTIVE")
|
|
1128
|
+
);
|
|
1129
|
+
Log.d("CameraPreview", "3. COMPUTED POSITION - x=" + computedX + ", y=" + computedY);
|
|
1130
|
+
Log.d("CameraPreview", "4. COMPUTED SIZE - width=" + computedWidth + ", height=" + computedHeight);
|
|
1131
|
+
Log.d("CameraPreview", "=== COORDINATE DEBUG ===");
|
|
1132
|
+
Log.d("CameraPreview", "WebView getLeft/getTop: (" + webViewLeft + ", " + webViewTop + ")");
|
|
1133
|
+
Log.d(
|
|
1134
|
+
"CameraPreview",
|
|
1135
|
+
"WebView locationInWindow: (" + webViewLocationInWindow[0] + ", " + webViewLocationInWindow[1] + ")"
|
|
1136
|
+
);
|
|
1137
|
+
Log.d(
|
|
1138
|
+
"CameraPreview",
|
|
1139
|
+
"WebView locationOnScreen: (" + webViewLocationOnScreen[0] + ", " + webViewLocationOnScreen[1] + ")"
|
|
1140
|
+
);
|
|
1141
|
+
Log.d("CameraPreview", "Parent locationInWindow: (" + parentLocationInWindow[0] + ", " + parentLocationInWindow[1] + ")");
|
|
1142
|
+
Log.d("CameraPreview", "Parent locationOnScreen: (" + parentLocationOnScreen[0] + ", " + parentLocationOnScreen[1] + ")");
|
|
1143
|
+
Log.d("CameraPreview", "Parent class: " + webViewParent.getClass().getSimpleName());
|
|
1144
|
+
Log.d("CameraPreview", "Requested position (logical): (" + x + ", " + y + ")");
|
|
1145
|
+
Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
|
|
1146
|
+
Log.d("CameraPreview", "Final computed position (no offset): (" + computedX + ", " + computedY + ")");
|
|
1147
|
+
Log.d("CameraPreview", "5. IS_CENTERED - " + (x == -1 || y == -1));
|
|
1148
|
+
Log.d("CameraPreview", "========================");
|
|
1149
|
+
|
|
1150
|
+
// Pass along whether we're centering so CameraXView knows not to add insets
|
|
1151
|
+
boolean isCentered = (x == -1 || y == -1);
|
|
1152
|
+
|
|
1153
|
+
CameraSessionConfiguration config = new CameraSessionConfiguration(
|
|
1154
|
+
finalDeviceId,
|
|
1155
|
+
position,
|
|
1156
|
+
computedX,
|
|
1157
|
+
computedY,
|
|
1158
|
+
computedWidth,
|
|
1159
|
+
computedHeight,
|
|
1160
|
+
paddingBottom,
|
|
1161
|
+
toBack,
|
|
1162
|
+
storeToFile,
|
|
1163
|
+
enableOpacity,
|
|
1164
|
+
disableExifHeaderStripping,
|
|
1165
|
+
disableAudio,
|
|
1166
|
+
1.0f,
|
|
1167
|
+
aspectRatio,
|
|
1168
|
+
gridMode,
|
|
1169
|
+
disableFocusIndicator,
|
|
1170
|
+
enableVideoMode
|
|
1171
|
+
);
|
|
1172
|
+
config.setTargetZoom(finalTargetZoom);
|
|
1173
|
+
config.setCentered(isCentered);
|
|
1174
|
+
|
|
1175
|
+
bridge.saveCall(call);
|
|
1176
|
+
cameraStartCallbackId = call.getCallbackId();
|
|
1177
|
+
cameraXView.startSession(config);
|
|
1178
|
+
|
|
1179
|
+
// Setup orientation listener to mirror iOS screenResize emission
|
|
1180
|
+
if (orientationListener == null) {
|
|
1181
|
+
lastOrientation = getContext().getResources().getConfiguration().orientation;
|
|
1182
|
+
lastOrientationStr = getDeviceOrientationString();
|
|
1183
|
+
orientationListener = new OrientationEventListener(getContext()) {
|
|
1184
|
+
@Override
|
|
1185
|
+
public void onOrientationChanged(int orientation) {
|
|
1186
|
+
if (orientation == ORIENTATION_UNKNOWN) return;
|
|
1187
|
+
int current = getContext().getResources().getConfiguration().orientation;
|
|
1188
|
+
String currentStr = getDeviceOrientationString();
|
|
1189
|
+
if (current != lastOrientation || !Objects.equals(currentStr, lastOrientationStr)) {
|
|
1190
|
+
lastOrientation = current;
|
|
1191
|
+
lastOrientationStr = currentStr;
|
|
1192
|
+
// Post to next frame so WebView has updated bounds before we recompute layout
|
|
1193
|
+
getBridge().getActivity().getWindow().getDecorView().post(() -> handleOrientationChange());
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
if (orientationListener.canDetectOrientation()) {
|
|
1198
|
+
orientationListener.enable();
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private void handleOrientationChange() {
|
|
1205
|
+
if (cameraXView == null || !cameraXView.isRunning()) return;
|
|
1206
|
+
|
|
1207
|
+
Log.d(TAG, "======================== ORIENTATION CHANGE DETECTED ========================");
|
|
1208
|
+
|
|
1209
|
+
// Get comprehensive display and orientation information
|
|
1210
|
+
android.util.DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
|
|
1211
|
+
int screenWidthPx = metrics.widthPixels;
|
|
1212
|
+
int screenHeightPx = metrics.heightPixels;
|
|
1213
|
+
float density = metrics.density;
|
|
1214
|
+
int screenWidthDp = (int) (screenWidthPx / density);
|
|
1215
|
+
int screenHeightDp = (int) (screenHeightPx / density);
|
|
1216
|
+
|
|
1217
|
+
int current = getContext().getResources().getConfiguration().orientation;
|
|
1218
|
+
Log.d(TAG, "New orientation: " + current + " (1=PORTRAIT, 2=LANDSCAPE)");
|
|
1610
1219
|
Log.d(
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
newWebViewHeight
|
|
1220
|
+
TAG,
|
|
1221
|
+
"Screen dimensions - Pixels: " +
|
|
1222
|
+
screenWidthPx +
|
|
1223
|
+
"x" +
|
|
1224
|
+
screenHeightPx +
|
|
1225
|
+
", DP: " +
|
|
1226
|
+
screenWidthDp +
|
|
1227
|
+
"x" +
|
|
1228
|
+
screenHeightDp +
|
|
1229
|
+
", Density: " +
|
|
1230
|
+
density
|
|
1623
1231
|
);
|
|
1624
1232
|
|
|
1625
|
-
//
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1233
|
+
// Get WebView dimensions before rotation
|
|
1234
|
+
WebView webView = getBridge().getWebView();
|
|
1235
|
+
int webViewWidth = webView.getWidth();
|
|
1236
|
+
int webViewHeight = webView.getHeight();
|
|
1237
|
+
Log.d(TAG, "WebView dimensions: " + webViewWidth + "x" + webViewHeight);
|
|
1238
|
+
|
|
1239
|
+
// Get current preview bounds before rotation
|
|
1240
|
+
int[] oldBounds = cameraXView.getCurrentPreviewBounds();
|
|
1241
|
+
Log.d(
|
|
1629
1242
|
TAG,
|
|
1630
|
-
"
|
|
1631
|
-
|
|
1243
|
+
"Current preview bounds before rotation: x=" +
|
|
1244
|
+
oldBounds[0] +
|
|
1632
1245
|
", y=" +
|
|
1633
|
-
|
|
1246
|
+
oldBounds[1] +
|
|
1634
1247
|
", width=" +
|
|
1635
|
-
|
|
1248
|
+
oldBounds[2] +
|
|
1636
1249
|
", height=" +
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
(
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
)
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
.
|
|
1678
|
-
.
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
.
|
|
1250
|
+
oldBounds[3]
|
|
1251
|
+
);
|
|
1252
|
+
|
|
1253
|
+
getBridge()
|
|
1254
|
+
.getActivity()
|
|
1255
|
+
.runOnUiThread(() -> {
|
|
1256
|
+
// Create and show a black full-screen overlay during rotation
|
|
1257
|
+
ViewGroup rootView = (ViewGroup) getBridge().getActivity().getWindow().getDecorView().getRootView();
|
|
1258
|
+
|
|
1259
|
+
// Remove any existing overlay
|
|
1260
|
+
if (rotationOverlay != null && rotationOverlay.getParent() != null) {
|
|
1261
|
+
((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Create new black overlay
|
|
1265
|
+
rotationOverlay = new View(getContext());
|
|
1266
|
+
rotationOverlay.setBackgroundColor(Color.BLACK);
|
|
1267
|
+
ViewGroup.LayoutParams overlayParams = new ViewGroup.LayoutParams(
|
|
1268
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
1269
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
1270
|
+
);
|
|
1271
|
+
rotationOverlay.setLayoutParams(overlayParams);
|
|
1272
|
+
rootView.addView(rotationOverlay);
|
|
1273
|
+
|
|
1274
|
+
// Reapply current aspect ratio to recompute layout, then emit screenResize
|
|
1275
|
+
String ar = cameraXView.getAspectRatio();
|
|
1276
|
+
Log.d(TAG, "Reapplying aspect ratio: " + ar);
|
|
1277
|
+
|
|
1278
|
+
// Re-get dimensions after potential layout pass
|
|
1279
|
+
android.util.DisplayMetrics newMetrics = getContext().getResources().getDisplayMetrics();
|
|
1280
|
+
int newScreenWidthPx = newMetrics.widthPixels;
|
|
1281
|
+
int newScreenHeightPx = newMetrics.heightPixels;
|
|
1282
|
+
int newWebViewWidth = webView.getWidth();
|
|
1283
|
+
int newWebViewHeight = webView.getHeight();
|
|
1284
|
+
|
|
1285
|
+
Log.d(TAG, "New screen dimensions after rotation: " + newScreenWidthPx + "x" + newScreenHeightPx);
|
|
1286
|
+
Log.d(TAG, "New WebView dimensions after rotation: " + newWebViewWidth + "x" + newWebViewHeight);
|
|
1287
|
+
|
|
1288
|
+
// Force aspect ratio recalculation on orientation change
|
|
1289
|
+
cameraXView.forceAspectRatioRecalculation(ar, null, null, () -> {
|
|
1290
|
+
int[] bounds = cameraXView.getCurrentPreviewBounds();
|
|
1291
|
+
Log.d(
|
|
1292
|
+
TAG,
|
|
1293
|
+
"New bounds after orientation change: x=" +
|
|
1294
|
+
bounds[0] +
|
|
1295
|
+
", y=" +
|
|
1296
|
+
bounds[1] +
|
|
1297
|
+
", width=" +
|
|
1298
|
+
bounds[2] +
|
|
1299
|
+
", height=" +
|
|
1300
|
+
bounds[3]
|
|
1301
|
+
);
|
|
1302
|
+
Log.d(
|
|
1303
|
+
TAG,
|
|
1304
|
+
"Bounds change: deltaX=" +
|
|
1305
|
+
(bounds[0] - oldBounds[0]) +
|
|
1306
|
+
", deltaY=" +
|
|
1307
|
+
(bounds[1] - oldBounds[1]) +
|
|
1308
|
+
", deltaWidth=" +
|
|
1309
|
+
(bounds[2] - oldBounds[2]) +
|
|
1310
|
+
", deltaHeight=" +
|
|
1311
|
+
(bounds[3] - oldBounds[3])
|
|
1312
|
+
);
|
|
1313
|
+
|
|
1314
|
+
JSObject data = new JSObject();
|
|
1315
|
+
data.put("x", bounds[0]);
|
|
1316
|
+
data.put("y", bounds[1]);
|
|
1317
|
+
data.put("width", bounds[2]);
|
|
1318
|
+
data.put("height", bounds[3]);
|
|
1319
|
+
notifyListeners("screenResize", data);
|
|
1320
|
+
|
|
1321
|
+
// Also emit orientationChange with a unified string value matching iOS
|
|
1322
|
+
String o = getDeviceOrientationString();
|
|
1323
|
+
JSObject oData = new JSObject();
|
|
1324
|
+
oData.put("orientation", o);
|
|
1325
|
+
notifyListeners("orientationChange", oData);
|
|
1326
|
+
|
|
1327
|
+
// Don't remove the overlay here - wait for camera to fully start
|
|
1328
|
+
// The overlay will be removed after a delay to ensure camera is stable
|
|
1329
|
+
if (rotationOverlay != null && rotationOverlay.getParent() != null) {
|
|
1330
|
+
// Shorter delay for faster transition
|
|
1331
|
+
int delay = "4:3".equals(ar) ? 200 : 150;
|
|
1332
|
+
rotationOverlay.postDelayed(
|
|
1333
|
+
() -> {
|
|
1334
|
+
if (rotationOverlay != null && rotationOverlay.getParent() != null) {
|
|
1335
|
+
rotationOverlay
|
|
1336
|
+
.animate()
|
|
1337
|
+
.alpha(0f)
|
|
1338
|
+
.setDuration(100) // Faster fade out
|
|
1339
|
+
.withEndAction(() -> {
|
|
1340
|
+
if (rotationOverlay != null && rotationOverlay.getParent() != null) {
|
|
1341
|
+
((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
|
|
1342
|
+
rotationOverlay = null;
|
|
1343
|
+
}
|
|
1344
|
+
})
|
|
1345
|
+
.start();
|
|
1346
|
+
}
|
|
1347
|
+
},
|
|
1348
|
+
delay
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
Log.d(TAG, "================================================================================");
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Compute a canonical orientation string matching iOS values:
|
|
1359
|
+
* "portrait", "portrait-upside-down", "landscape-left", "landscape-right", or "unknown".
|
|
1360
|
+
* Uses display rotation when available, with a fallback to configuration orientation.
|
|
1361
|
+
*/
|
|
1362
|
+
private String getDeviceOrientationString() {
|
|
1363
|
+
try {
|
|
1364
|
+
int rotation = -1;
|
|
1365
|
+
// Try to obtain display rotation in a backward/forward-compatible way
|
|
1366
|
+
if (android.os.Build.VERSION.SDK_INT >= 30) {
|
|
1367
|
+
android.view.Display display = getBridge().getActivity().getDisplay();
|
|
1368
|
+
if (display != null) {
|
|
1369
|
+
rotation = display.getRotation();
|
|
1690
1370
|
}
|
|
1691
|
-
|
|
1692
|
-
|
|
1371
|
+
} else {
|
|
1372
|
+
android.view.Display display = getBridge().getActivity().getWindowManager().getDefaultDisplay();
|
|
1373
|
+
if (display != null) {
|
|
1374
|
+
rotation = display.getRotation();
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
if (rotation == android.view.Surface.ROTATION_0) {
|
|
1379
|
+
return "portrait";
|
|
1380
|
+
} else if (rotation == android.view.Surface.ROTATION_90) {
|
|
1381
|
+
return "landscape-right";
|
|
1382
|
+
} else if (rotation == android.view.Surface.ROTATION_180) {
|
|
1383
|
+
return "portrait-upside-down";
|
|
1384
|
+
} else if (rotation == android.view.Surface.ROTATION_270) {
|
|
1385
|
+
return "landscape-left";
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Fallback to configuration if rotation unavailable
|
|
1389
|
+
int orientation = getContext().getResources().getConfiguration().orientation;
|
|
1390
|
+
if (orientation == Configuration.ORIENTATION_PORTRAIT) return "portrait";
|
|
1391
|
+
if (orientation == Configuration.ORIENTATION_LANDSCAPE) return "landscape-right"; // default, avoid generic
|
|
1392
|
+
return "unknown";
|
|
1393
|
+
} catch (Throwable t) {
|
|
1394
|
+
Log.w(TAG, "Failed to get precise orientation, falling back: " + t);
|
|
1395
|
+
int orientation = getContext().getResources().getConfiguration().orientation;
|
|
1396
|
+
if (orientation == Configuration.ORIENTATION_PORTRAIT) return "portrait";
|
|
1397
|
+
if (orientation == Configuration.ORIENTATION_LANDSCAPE) return "landscape-right"; // default, avoid generic
|
|
1398
|
+
return "unknown";
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
@Override
|
|
1403
|
+
public void onPictureTaken(String base64, JSONObject exif) {
|
|
1404
|
+
PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
|
|
1405
|
+
if (pluginCall == null) {
|
|
1406
|
+
Log.e("CameraPreview", "onPictureTaken: captureCallbackId is null");
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
JSObject result = new JSObject();
|
|
1410
|
+
result.put("value", base64);
|
|
1411
|
+
result.put("exif", exif);
|
|
1412
|
+
pluginCall.resolve(result);
|
|
1413
|
+
bridge.releaseCall(pluginCall);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
@Override
|
|
1417
|
+
public void onPictureTakenError(String message) {
|
|
1418
|
+
PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
|
|
1419
|
+
if (pluginCall == null) {
|
|
1420
|
+
Log.e("CameraPreview", "onPictureTakenError: captureCallbackId is null");
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
pluginCall.reject(message);
|
|
1424
|
+
bridge.releaseCall(pluginCall);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
@Override
|
|
1428
|
+
public void onCameraStopped(CameraXView source) {
|
|
1429
|
+
if (cameraXView != null && cameraXView != source) {
|
|
1430
|
+
Log.d(TAG, "onCameraStopped: ignoring callback from stale instance");
|
|
1431
|
+
return;
|
|
1432
|
+
}
|
|
1433
|
+
// Ensure reference is cleared once the originating CameraXView has fully stopped
|
|
1434
|
+
if (cameraXView == source) {
|
|
1435
|
+
cameraXView = null;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
PluginCall queuedCall = null;
|
|
1439
|
+
synchronized (pendingStartLock) {
|
|
1440
|
+
if (pendingStartCall != null) {
|
|
1441
|
+
queuedCall = pendingStartCall;
|
|
1442
|
+
pendingStartCall = null;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
if (queuedCall != null) {
|
|
1447
|
+
PluginCall finalQueuedCall = queuedCall;
|
|
1448
|
+
Log.d(TAG, "onCameraStopped: replaying pending start request");
|
|
1449
|
+
getBridge().getActivity().runOnUiThread(() -> start(finalQueuedCall));
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
private JSObject getViewSize(double x, double y, double width, double height) {
|
|
1454
|
+
JSObject ret = new JSObject();
|
|
1455
|
+
// Return values with proper rounding to avoid gaps
|
|
1456
|
+
// For positions (x, y): ceil to avoid gaps at top/left
|
|
1457
|
+
// For dimensions (width, height): floor to avoid gaps at bottom/right
|
|
1458
|
+
ret.put("x", Math.ceil(x));
|
|
1459
|
+
ret.put("y", Math.ceil(y));
|
|
1460
|
+
ret.put("width", Math.floor(width));
|
|
1461
|
+
ret.put("height", Math.floor(height));
|
|
1462
|
+
return ret;
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
@Override
|
|
1466
|
+
public void onCameraStarted(int width, int height, int x, int y) {
|
|
1467
|
+
PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
|
|
1468
|
+
if (call != null) {
|
|
1469
|
+
// Convert pixel values back to logical units
|
|
1470
|
+
DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
|
|
1471
|
+
float pixelRatio = metrics.density;
|
|
1472
|
+
|
|
1473
|
+
// When WebView is offset from the top (e.g., below status bar),
|
|
1474
|
+
// we need to convert between JavaScript coordinates (relative to WebView)
|
|
1475
|
+
// and native coordinates (relative to screen)
|
|
1476
|
+
WebView webView = getBridge().getWebView();
|
|
1477
|
+
int webViewTopInset = 0;
|
|
1478
|
+
boolean isEdgeToEdgeActive = false;
|
|
1479
|
+
if (webView != null) {
|
|
1480
|
+
int[] location = new int[2];
|
|
1481
|
+
webView.getLocationOnScreen(location);
|
|
1482
|
+
webViewTopInset = location[1];
|
|
1483
|
+
isEdgeToEdgeActive = webViewTopInset > 0;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// Only convert to relative position if edge-to-edge is active
|
|
1487
|
+
int relativeY = isEdgeToEdgeActive ? (y - webViewTopInset) : y;
|
|
1488
|
+
|
|
1489
|
+
Log.d("CameraPreview", "========================");
|
|
1490
|
+
Log.d("CameraPreview", "CAMERA STARTED - POSITION RETURNED:");
|
|
1491
|
+
Log.d("CameraPreview", "7. RETURNED (pixels) - x=" + x + ", y=" + y + ", width=" + width + ", height=" + height);
|
|
1492
|
+
Log.d("CameraPreview", "8. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE" : "INACTIVE"));
|
|
1493
|
+
Log.d("CameraPreview", "9. WEBVIEW INSET - " + webViewTopInset);
|
|
1494
|
+
Log.d(
|
|
1495
|
+
"CameraPreview",
|
|
1496
|
+
"10. RELATIVE Y - " + relativeY + " (y=" + y + (isEdgeToEdgeActive ? " - inset=" + webViewTopInset : " unchanged") + ")"
|
|
1497
|
+
);
|
|
1498
|
+
Log.d(
|
|
1499
|
+
"CameraPreview",
|
|
1500
|
+
"11. RETURNED (logical) - x=" +
|
|
1501
|
+
(x / pixelRatio) +
|
|
1502
|
+
", y=" +
|
|
1503
|
+
(relativeY / pixelRatio) +
|
|
1504
|
+
", width=" +
|
|
1505
|
+
(width / pixelRatio) +
|
|
1506
|
+
", height=" +
|
|
1507
|
+
(height / pixelRatio)
|
|
1508
|
+
);
|
|
1509
|
+
Log.d("CameraPreview", "12. PIXEL RATIO - " + pixelRatio);
|
|
1510
|
+
Log.d("CameraPreview", "========================");
|
|
1511
|
+
|
|
1512
|
+
// Calculate logical values with proper rounding to avoid sub-pixel issues
|
|
1513
|
+
double logicalWidth = width / pixelRatio;
|
|
1514
|
+
double logicalHeight = height / pixelRatio;
|
|
1515
|
+
double logicalX = x / pixelRatio;
|
|
1516
|
+
double logicalY = relativeY / pixelRatio;
|
|
1517
|
+
|
|
1518
|
+
JSObject result = getViewSize(logicalX, logicalY, logicalWidth, logicalHeight);
|
|
1519
|
+
|
|
1520
|
+
// Log exact calculations to debug one-pixel difference
|
|
1521
|
+
Log.d("CameraPreview", "========================");
|
|
1522
|
+
Log.d("CameraPreview", "FINAL POSITION CALCULATIONS:");
|
|
1523
|
+
Log.d("CameraPreview", "Pixel values: x=" + x + ", y=" + relativeY + ", width=" + width + ", height=" + height);
|
|
1524
|
+
Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
|
|
1525
|
+
Log.d(
|
|
1526
|
+
"CameraPreview",
|
|
1527
|
+
"Logical values (exact): x=" + logicalX + ", y=" + logicalY + ", width=" + logicalWidth + ", height=" + logicalHeight
|
|
1528
|
+
);
|
|
1529
|
+
Log.d(
|
|
1530
|
+
"CameraPreview",
|
|
1531
|
+
"Logical values (rounded): x=" +
|
|
1532
|
+
Math.round(logicalX) +
|
|
1533
|
+
", y=" +
|
|
1534
|
+
Math.round(logicalY) +
|
|
1535
|
+
", width=" +
|
|
1536
|
+
Math.round(logicalWidth) +
|
|
1537
|
+
", height=" +
|
|
1538
|
+
Math.round(logicalHeight)
|
|
1693
1539
|
);
|
|
1694
|
-
}
|
|
1695
1540
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
return "landscape-right";
|
|
1732
|
-
} else if (rotation == android.view.Surface.ROTATION_180) {
|
|
1733
|
-
return "portrait-upside-down";
|
|
1734
|
-
} else if (rotation == android.view.Surface.ROTATION_270) {
|
|
1735
|
-
return "landscape-left";
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
// Fallback to configuration if rotation unavailable
|
|
1739
|
-
int orientation = getContext()
|
|
1740
|
-
.getResources()
|
|
1741
|
-
.getConfiguration()
|
|
1742
|
-
.orientation;
|
|
1743
|
-
if (orientation == Configuration.ORIENTATION_PORTRAIT) return "portrait";
|
|
1744
|
-
if (
|
|
1745
|
-
orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
1746
|
-
) return "landscape-right"; // default, avoid generic
|
|
1747
|
-
return "unknown";
|
|
1748
|
-
} catch (Throwable t) {
|
|
1749
|
-
Log.w(TAG, "Failed to get precise orientation, falling back: " + t);
|
|
1750
|
-
int orientation = getContext()
|
|
1751
|
-
.getResources()
|
|
1752
|
-
.getConfiguration()
|
|
1753
|
-
.orientation;
|
|
1754
|
-
if (orientation == Configuration.ORIENTATION_PORTRAIT) return "portrait";
|
|
1755
|
-
if (
|
|
1756
|
-
orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
1757
|
-
) return "landscape-right"; // default, avoid generic
|
|
1758
|
-
return "unknown";
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
@Override
|
|
1763
|
-
public void onPictureTaken(String base64, JSONObject exif) {
|
|
1764
|
-
PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
|
|
1765
|
-
if (pluginCall == null) {
|
|
1766
|
-
Log.e("CameraPreview", "onPictureTaken: captureCallbackId is null");
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
JSObject result = new JSObject();
|
|
1770
|
-
result.put("value", base64);
|
|
1771
|
-
result.put("exif", exif);
|
|
1772
|
-
pluginCall.resolve(result);
|
|
1773
|
-
bridge.releaseCall(pluginCall);
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
@Override
|
|
1777
|
-
public void onPictureTakenError(String message) {
|
|
1778
|
-
PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
|
|
1779
|
-
if (pluginCall == null) {
|
|
1780
|
-
Log.e("CameraPreview", "onPictureTakenError: captureCallbackId is null");
|
|
1781
|
-
return;
|
|
1782
|
-
}
|
|
1783
|
-
pluginCall.reject(message);
|
|
1784
|
-
bridge.releaseCall(pluginCall);
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
@Override
|
|
1788
|
-
public void onCameraStopped(CameraXView source) {
|
|
1789
|
-
if (cameraXView != null && cameraXView != source) {
|
|
1790
|
-
Log.d(TAG, "onCameraStopped: ignoring callback from stale instance");
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
// Ensure reference is cleared once the originating CameraXView has fully stopped
|
|
1794
|
-
if (cameraXView == source) {
|
|
1795
|
-
cameraXView = null;
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
PluginCall queuedCall = null;
|
|
1799
|
-
synchronized (pendingStartLock) {
|
|
1800
|
-
if (pendingStartCall != null) {
|
|
1801
|
-
queuedCall = pendingStartCall;
|
|
1802
|
-
pendingStartCall = null;
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
if (queuedCall != null) {
|
|
1807
|
-
PluginCall finalQueuedCall = queuedCall;
|
|
1808
|
-
Log.d(TAG, "onCameraStopped: replaying pending start request");
|
|
1809
|
-
getBridge().getActivity().runOnUiThread(() -> start(finalQueuedCall));
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
private JSObject getViewSize(
|
|
1814
|
-
double x,
|
|
1815
|
-
double y,
|
|
1816
|
-
double width,
|
|
1817
|
-
double height
|
|
1818
|
-
) {
|
|
1819
|
-
JSObject ret = new JSObject();
|
|
1820
|
-
// Return values with proper rounding to avoid gaps
|
|
1821
|
-
// For positions (x, y): ceil to avoid gaps at top/left
|
|
1822
|
-
// For dimensions (width, height): floor to avoid gaps at bottom/right
|
|
1823
|
-
ret.put("x", Math.ceil(x));
|
|
1824
|
-
ret.put("y", Math.ceil(y));
|
|
1825
|
-
ret.put("width", Math.floor(width));
|
|
1826
|
-
ret.put("height", Math.floor(height));
|
|
1827
|
-
return ret;
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
@Override
|
|
1831
|
-
public void onCameraStarted(int width, int height, int x, int y) {
|
|
1832
|
-
PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
|
|
1833
|
-
if (call != null) {
|
|
1834
|
-
// Convert pixel values back to logical units
|
|
1835
|
-
DisplayMetrics metrics = getBridge()
|
|
1836
|
-
.getActivity()
|
|
1837
|
-
.getResources()
|
|
1838
|
-
.getDisplayMetrics();
|
|
1839
|
-
float pixelRatio = metrics.density;
|
|
1840
|
-
|
|
1841
|
-
// When WebView is offset from the top (e.g., below status bar),
|
|
1842
|
-
// we need to convert between JavaScript coordinates (relative to WebView)
|
|
1843
|
-
// and native coordinates (relative to screen)
|
|
1844
|
-
WebView webView = getBridge().getWebView();
|
|
1845
|
-
int webViewTopInset = 0;
|
|
1846
|
-
boolean isEdgeToEdgeActive = false;
|
|
1847
|
-
if (webView != null) {
|
|
1848
|
-
int[] location = new int[2];
|
|
1849
|
-
webView.getLocationOnScreen(location);
|
|
1850
|
-
webViewTopInset = location[1];
|
|
1851
|
-
isEdgeToEdgeActive = webViewTopInset > 0;
|
|
1852
|
-
}
|
|
1853
|
-
|
|
1854
|
-
// Only convert to relative position if edge-to-edge is active
|
|
1855
|
-
int relativeY = isEdgeToEdgeActive ? (y - webViewTopInset) : y;
|
|
1856
|
-
|
|
1857
|
-
Log.d("CameraPreview", "========================");
|
|
1858
|
-
Log.d("CameraPreview", "CAMERA STARTED - POSITION RETURNED:");
|
|
1859
|
-
Log.d(
|
|
1860
|
-
"CameraPreview",
|
|
1861
|
-
"7. RETURNED (pixels) - x=" +
|
|
1862
|
-
x +
|
|
1863
|
-
", y=" +
|
|
1864
|
-
y +
|
|
1865
|
-
", width=" +
|
|
1866
|
-
width +
|
|
1867
|
-
", height=" +
|
|
1868
|
-
height
|
|
1869
|
-
);
|
|
1870
|
-
Log.d(
|
|
1871
|
-
"CameraPreview",
|
|
1872
|
-
"8. EDGE-TO-EDGE - " + (isEdgeToEdgeActive ? "ACTIVE" : "INACTIVE")
|
|
1873
|
-
);
|
|
1874
|
-
Log.d("CameraPreview", "9. WEBVIEW INSET - " + webViewTopInset);
|
|
1875
|
-
Log.d(
|
|
1876
|
-
"CameraPreview",
|
|
1877
|
-
"10. RELATIVE Y - " +
|
|
1878
|
-
relativeY +
|
|
1879
|
-
" (y=" +
|
|
1880
|
-
y +
|
|
1881
|
-
(isEdgeToEdgeActive ? " - inset=" + webViewTopInset : " unchanged") +
|
|
1882
|
-
")"
|
|
1883
|
-
);
|
|
1884
|
-
Log.d(
|
|
1885
|
-
"CameraPreview",
|
|
1886
|
-
"11. RETURNED (logical) - x=" +
|
|
1887
|
-
(x / pixelRatio) +
|
|
1888
|
-
", y=" +
|
|
1889
|
-
(relativeY / pixelRatio) +
|
|
1890
|
-
", width=" +
|
|
1891
|
-
(width / pixelRatio) +
|
|
1892
|
-
", height=" +
|
|
1893
|
-
(height / pixelRatio)
|
|
1894
|
-
);
|
|
1895
|
-
Log.d("CameraPreview", "12. PIXEL RATIO - " + pixelRatio);
|
|
1896
|
-
Log.d("CameraPreview", "========================");
|
|
1897
|
-
|
|
1898
|
-
// Calculate logical values with proper rounding to avoid sub-pixel issues
|
|
1899
|
-
double logicalWidth = width / pixelRatio;
|
|
1900
|
-
double logicalHeight = height / pixelRatio;
|
|
1901
|
-
double logicalX = x / pixelRatio;
|
|
1902
|
-
double logicalY = relativeY / pixelRatio;
|
|
1903
|
-
|
|
1904
|
-
JSObject result = getViewSize(
|
|
1905
|
-
logicalX,
|
|
1906
|
-
logicalY,
|
|
1907
|
-
logicalWidth,
|
|
1908
|
-
logicalHeight
|
|
1909
|
-
);
|
|
1910
|
-
|
|
1911
|
-
// Log exact calculations to debug one-pixel difference
|
|
1912
|
-
Log.d("CameraPreview", "========================");
|
|
1913
|
-
Log.d("CameraPreview", "FINAL POSITION CALCULATIONS:");
|
|
1914
|
-
Log.d(
|
|
1915
|
-
"CameraPreview",
|
|
1916
|
-
"Pixel values: x=" +
|
|
1917
|
-
x +
|
|
1918
|
-
", y=" +
|
|
1919
|
-
relativeY +
|
|
1920
|
-
", width=" +
|
|
1921
|
-
width +
|
|
1922
|
-
", height=" +
|
|
1923
|
-
height
|
|
1924
|
-
);
|
|
1925
|
-
Log.d("CameraPreview", "Pixel ratio: " + pixelRatio);
|
|
1926
|
-
Log.d(
|
|
1927
|
-
"CameraPreview",
|
|
1928
|
-
"Logical values (exact): x=" +
|
|
1929
|
-
logicalX +
|
|
1930
|
-
", y=" +
|
|
1931
|
-
logicalY +
|
|
1932
|
-
", width=" +
|
|
1933
|
-
logicalWidth +
|
|
1934
|
-
", height=" +
|
|
1935
|
-
logicalHeight
|
|
1936
|
-
);
|
|
1937
|
-
Log.d(
|
|
1938
|
-
"CameraPreview",
|
|
1939
|
-
"Logical values (rounded): x=" +
|
|
1940
|
-
Math.round(logicalX) +
|
|
1941
|
-
", y=" +
|
|
1942
|
-
Math.round(logicalY) +
|
|
1943
|
-
", width=" +
|
|
1944
|
-
Math.round(logicalWidth) +
|
|
1945
|
-
", height=" +
|
|
1946
|
-
Math.round(logicalHeight)
|
|
1947
|
-
);
|
|
1948
|
-
|
|
1949
|
-
// Check if previewContainer has any padding or margin that might cause offset
|
|
1950
|
-
if (cameraXView != null) {
|
|
1951
|
-
View previewContainer = cameraXView.getPreviewContainer();
|
|
1952
|
-
if (previewContainer != null) {
|
|
1953
|
-
Log.d(
|
|
1954
|
-
"CameraPreview",
|
|
1955
|
-
"PreviewContainer padding: left=" +
|
|
1956
|
-
previewContainer.getPaddingLeft() +
|
|
1957
|
-
", top=" +
|
|
1958
|
-
previewContainer.getPaddingTop() +
|
|
1959
|
-
", right=" +
|
|
1960
|
-
previewContainer.getPaddingRight() +
|
|
1961
|
-
", bottom=" +
|
|
1962
|
-
previewContainer.getPaddingBottom()
|
|
1963
|
-
);
|
|
1964
|
-
ViewGroup.LayoutParams params = previewContainer.getLayoutParams();
|
|
1965
|
-
if (params instanceof ViewGroup.MarginLayoutParams) {
|
|
1966
|
-
ViewGroup.MarginLayoutParams marginParams =
|
|
1967
|
-
(ViewGroup.MarginLayoutParams) params;
|
|
1541
|
+
// Check if previewContainer has any padding or margin that might cause offset
|
|
1542
|
+
if (cameraXView != null) {
|
|
1543
|
+
View previewContainer = cameraXView.getPreviewContainer();
|
|
1544
|
+
if (previewContainer != null) {
|
|
1545
|
+
Log.d(
|
|
1546
|
+
"CameraPreview",
|
|
1547
|
+
"PreviewContainer padding: left=" +
|
|
1548
|
+
previewContainer.getPaddingLeft() +
|
|
1549
|
+
", top=" +
|
|
1550
|
+
previewContainer.getPaddingTop() +
|
|
1551
|
+
", right=" +
|
|
1552
|
+
previewContainer.getPaddingRight() +
|
|
1553
|
+
", bottom=" +
|
|
1554
|
+
previewContainer.getPaddingBottom()
|
|
1555
|
+
);
|
|
1556
|
+
ViewGroup.LayoutParams params = previewContainer.getLayoutParams();
|
|
1557
|
+
if (params instanceof ViewGroup.MarginLayoutParams) {
|
|
1558
|
+
ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) params;
|
|
1559
|
+
Log.d(
|
|
1560
|
+
"CameraPreview",
|
|
1561
|
+
"PreviewContainer margins: left=" +
|
|
1562
|
+
marginParams.leftMargin +
|
|
1563
|
+
", top=" +
|
|
1564
|
+
marginParams.topMargin +
|
|
1565
|
+
", right=" +
|
|
1566
|
+
marginParams.rightMargin +
|
|
1567
|
+
", bottom=" +
|
|
1568
|
+
marginParams.bottomMargin
|
|
1569
|
+
);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
Log.d("CameraPreview", "========================");
|
|
1574
|
+
|
|
1575
|
+
// Log what we're returning
|
|
1968
1576
|
Log.d(
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1577
|
+
"CameraPreview",
|
|
1578
|
+
"Returning to JS - x: " +
|
|
1579
|
+
logicalX +
|
|
1580
|
+
" (from " +
|
|
1581
|
+
logicalX +
|
|
1582
|
+
"), y: " +
|
|
1583
|
+
logicalY +
|
|
1584
|
+
" (from " +
|
|
1585
|
+
logicalY +
|
|
1586
|
+
"), width: " +
|
|
1587
|
+
logicalWidth +
|
|
1588
|
+
" (from " +
|
|
1589
|
+
logicalWidth +
|
|
1590
|
+
"), height: " +
|
|
1591
|
+
logicalHeight +
|
|
1592
|
+
" (from " +
|
|
1593
|
+
logicalHeight +
|
|
1594
|
+
")"
|
|
1978
1595
|
);
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
1596
|
+
|
|
1597
|
+
call.resolve(result);
|
|
1598
|
+
bridge.releaseCall(call);
|
|
1599
|
+
cameraStartCallbackId = null; // Prevent re-use
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
@Override
|
|
1604
|
+
public void onSampleTaken(String result) {
|
|
1605
|
+
PluginCall call = bridge.getSavedCall(sampleCallbackId);
|
|
1606
|
+
if (call != null) {
|
|
1607
|
+
JSObject ret = new JSObject();
|
|
1608
|
+
ret.put("value", result);
|
|
1609
|
+
call.resolve(ret);
|
|
1610
|
+
bridge.releaseCall(call);
|
|
1611
|
+
sampleCallbackId = null;
|
|
1612
|
+
} else {
|
|
1613
|
+
Log.w("CameraPreview", "onSampleTaken: no pending call to resolve");
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
@Override
|
|
1618
|
+
public void onSampleTakenError(String message) {
|
|
1619
|
+
PluginCall call = bridge.getSavedCall(sampleCallbackId);
|
|
1620
|
+
if (call != null) {
|
|
1621
|
+
call.reject(message);
|
|
1622
|
+
bridge.releaseCall(call);
|
|
1623
|
+
sampleCallbackId = null;
|
|
1624
|
+
} else {
|
|
1625
|
+
Log.e("CameraPreview", "Sample taken error (no pending call): " + message);
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
@Override
|
|
1630
|
+
public void onCameraStartError(String message) {
|
|
1631
|
+
PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
|
|
1632
|
+
if (call != null) {
|
|
1633
|
+
call.reject(message);
|
|
1634
|
+
bridge.releaseCall(call);
|
|
1635
|
+
cameraStartCallbackId = null;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
@PluginMethod
|
|
1640
|
+
public void setAspectRatio(PluginCall call) {
|
|
1641
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
1642
|
+
call.reject("Camera is not running");
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
String aspectRatio = call.getString("aspectRatio", "4:3");
|
|
1646
|
+
Float x = call.getFloat("x");
|
|
1647
|
+
Float y = call.getFloat("y");
|
|
1648
|
+
|
|
1649
|
+
getActivity()
|
|
1650
|
+
.runOnUiThread(() -> {
|
|
1651
|
+
cameraXView.setAspectRatio(aspectRatio, x, y, () -> {
|
|
1652
|
+
// Return the actual preview bounds after layout and camera operations are complete
|
|
1653
|
+
int[] bounds = cameraXView.getCurrentPreviewBounds();
|
|
1654
|
+
JSObject ret = new JSObject();
|
|
1655
|
+
ret.put("x", bounds[0]);
|
|
1656
|
+
ret.put("y", bounds[1]);
|
|
1657
|
+
ret.put("width", bounds[2]);
|
|
1658
|
+
ret.put("height", bounds[3]);
|
|
1659
|
+
call.resolve(ret);
|
|
1660
|
+
});
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
@PluginMethod
|
|
1665
|
+
public void getAspectRatio(PluginCall call) {
|
|
1666
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
1667
|
+
call.reject("Camera is not running");
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
String aspectRatio = cameraXView.getAspectRatio();
|
|
1671
|
+
JSObject ret = new JSObject();
|
|
1672
|
+
ret.put("aspectRatio", aspectRatio);
|
|
1673
|
+
call.resolve(ret);
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
@PluginMethod
|
|
1677
|
+
public void setGridMode(PluginCall call) {
|
|
1678
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
1679
|
+
call.reject("Camera is not running");
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
String gridMode = call.getString("gridMode", "none");
|
|
1683
|
+
getActivity()
|
|
1684
|
+
.runOnUiThread(() -> {
|
|
1685
|
+
cameraXView.setGridMode(gridMode);
|
|
1686
|
+
call.resolve();
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
@PluginMethod
|
|
1691
|
+
public void getGridMode(PluginCall call) {
|
|
1692
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
1693
|
+
call.reject("Camera is not running");
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
JSObject ret = new JSObject();
|
|
1697
|
+
ret.put("gridMode", cameraXView.getGridMode());
|
|
1698
|
+
call.resolve(ret);
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
@PluginMethod
|
|
1702
|
+
public void getPreviewSize(PluginCall call) {
|
|
1703
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
1704
|
+
call.reject("Camera is not running");
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// Convert pixel values back to logical units
|
|
1709
|
+
DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
|
|
1710
|
+
float pixelRatio = metrics.density;
|
|
1711
|
+
|
|
1712
|
+
JSObject ret = new JSObject();
|
|
1713
|
+
// Use same rounding strategy as start method
|
|
1714
|
+
double x = Math.ceil(cameraXView.getPreviewX() / pixelRatio);
|
|
1715
|
+
double y = Math.ceil(cameraXView.getPreviewY() / pixelRatio);
|
|
1716
|
+
double width = Math.floor(cameraXView.getPreviewWidth() / pixelRatio);
|
|
1717
|
+
double height = Math.floor(cameraXView.getPreviewHeight() / pixelRatio);
|
|
1718
|
+
|
|
1719
|
+
Log.d("CameraPreview", "getPreviewSize: x=" + x + ", y=" + y + ", width=" + width + ", height=" + height);
|
|
1720
|
+
ret.put("x", x);
|
|
1721
|
+
ret.put("y", y);
|
|
1722
|
+
ret.put("width", width);
|
|
1723
|
+
ret.put("height", height);
|
|
1724
|
+
call.resolve(ret);
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
@PluginMethod
|
|
1728
|
+
public void setPreviewSize(PluginCall call) {
|
|
1729
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
1730
|
+
call.reject("Camera is not running");
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// Get values from call - null values will become 0
|
|
1735
|
+
Integer xParam = call.getInt("x");
|
|
1736
|
+
Integer yParam = call.getInt("y");
|
|
1737
|
+
Integer widthParam = call.getInt("width");
|
|
1738
|
+
Integer heightParam = call.getInt("height");
|
|
1739
|
+
|
|
1740
|
+
// Apply pixel ratio conversion to non-null values
|
|
1741
|
+
DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
|
|
1742
|
+
float pixelRatio = metrics.density;
|
|
1743
|
+
|
|
1744
|
+
// Check if edge-to-edge mode is active
|
|
1745
|
+
WebView webView = getBridge().getWebView();
|
|
1746
|
+
int webViewTopInset = 0;
|
|
1747
|
+
boolean isEdgeToEdgeActive = false;
|
|
1748
|
+
if (webView != null) {
|
|
1749
|
+
int[] location = new int[2];
|
|
1750
|
+
webView.getLocationOnScreen(location);
|
|
1751
|
+
webViewTopInset = location[1];
|
|
1752
|
+
isEdgeToEdgeActive = webViewTopInset > 0;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
int x = (xParam != null && xParam > 0) ? (int) (xParam * pixelRatio) : 0;
|
|
1756
|
+
int y = (yParam != null && yParam > 0) ? (int) (yParam * pixelRatio) : 0;
|
|
1757
|
+
|
|
1758
|
+
// Add edge-to-edge inset to Y if active
|
|
1759
|
+
if (isEdgeToEdgeActive && y > 0) {
|
|
1760
|
+
y += webViewTopInset;
|
|
1761
|
+
}
|
|
1762
|
+
int width = (widthParam != null && widthParam > 0) ? (int) (widthParam * pixelRatio) : 0;
|
|
1763
|
+
int height = (heightParam != null && heightParam > 0) ? (int) (heightParam * pixelRatio) : 0;
|
|
1764
|
+
|
|
1765
|
+
cameraXView.setPreviewSize(x, y, width, height, () -> {
|
|
1766
|
+
// Return the actual preview bounds after layout operations are complete
|
|
1767
|
+
int[] bounds = cameraXView.getCurrentPreviewBounds();
|
|
1768
|
+
JSObject ret = new JSObject();
|
|
1769
|
+
ret.put("x", bounds[0]);
|
|
1770
|
+
ret.put("y", bounds[1]);
|
|
1771
|
+
ret.put("width", bounds[2]);
|
|
1772
|
+
ret.put("height", bounds[3]);
|
|
1773
|
+
call.resolve(ret);
|
|
2072
1774
|
});
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
@PluginMethod
|
|
2114
|
-
public void getPreviewSize(PluginCall call) {
|
|
2115
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
2116
|
-
call.reject("Camera is not running");
|
|
2117
|
-
return;
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
// Convert pixel values back to logical units
|
|
2121
|
-
DisplayMetrics metrics = getBridge()
|
|
2122
|
-
.getActivity()
|
|
2123
|
-
.getResources()
|
|
2124
|
-
.getDisplayMetrics();
|
|
2125
|
-
float pixelRatio = metrics.density;
|
|
2126
|
-
|
|
2127
|
-
JSObject ret = new JSObject();
|
|
2128
|
-
// Use same rounding strategy as start method
|
|
2129
|
-
double x = Math.ceil(cameraXView.getPreviewX() / pixelRatio);
|
|
2130
|
-
double y = Math.ceil(cameraXView.getPreviewY() / pixelRatio);
|
|
2131
|
-
double width = Math.floor(cameraXView.getPreviewWidth() / pixelRatio);
|
|
2132
|
-
double height = Math.floor(cameraXView.getPreviewHeight() / pixelRatio);
|
|
2133
|
-
|
|
2134
|
-
Log.d(
|
|
2135
|
-
"CameraPreview",
|
|
2136
|
-
"getPreviewSize: x=" +
|
|
2137
|
-
x +
|
|
2138
|
-
", y=" +
|
|
2139
|
-
y +
|
|
2140
|
-
", width=" +
|
|
2141
|
-
width +
|
|
2142
|
-
", height=" +
|
|
2143
|
-
height
|
|
2144
|
-
);
|
|
2145
|
-
ret.put("x", x);
|
|
2146
|
-
ret.put("y", y);
|
|
2147
|
-
ret.put("width", width);
|
|
2148
|
-
ret.put("height", height);
|
|
2149
|
-
call.resolve(ret);
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
@PluginMethod
|
|
2153
|
-
public void setPreviewSize(PluginCall call) {
|
|
2154
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
2155
|
-
call.reject("Camera is not running");
|
|
2156
|
-
return;
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
// Get values from call - null values will become 0
|
|
2160
|
-
Integer xParam = call.getInt("x");
|
|
2161
|
-
Integer yParam = call.getInt("y");
|
|
2162
|
-
Integer widthParam = call.getInt("width");
|
|
2163
|
-
Integer heightParam = call.getInt("height");
|
|
2164
|
-
|
|
2165
|
-
// Apply pixel ratio conversion to non-null values
|
|
2166
|
-
DisplayMetrics metrics = getBridge()
|
|
2167
|
-
.getActivity()
|
|
2168
|
-
.getResources()
|
|
2169
|
-
.getDisplayMetrics();
|
|
2170
|
-
float pixelRatio = metrics.density;
|
|
2171
|
-
|
|
2172
|
-
// Check if edge-to-edge mode is active
|
|
2173
|
-
WebView webView = getBridge().getWebView();
|
|
2174
|
-
int webViewTopInset = 0;
|
|
2175
|
-
boolean isEdgeToEdgeActive = false;
|
|
2176
|
-
if (webView != null) {
|
|
2177
|
-
int[] location = new int[2];
|
|
2178
|
-
webView.getLocationOnScreen(location);
|
|
2179
|
-
webViewTopInset = location[1];
|
|
2180
|
-
isEdgeToEdgeActive = webViewTopInset > 0;
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
int x = (xParam != null && xParam > 0) ? (int) (xParam * pixelRatio) : 0;
|
|
2184
|
-
int y = (yParam != null && yParam > 0) ? (int) (yParam * pixelRatio) : 0;
|
|
2185
|
-
|
|
2186
|
-
// Add edge-to-edge inset to Y if active
|
|
2187
|
-
if (isEdgeToEdgeActive && y > 0) {
|
|
2188
|
-
y += webViewTopInset;
|
|
2189
|
-
}
|
|
2190
|
-
int width = (widthParam != null && widthParam > 0)
|
|
2191
|
-
? (int) (widthParam * pixelRatio)
|
|
2192
|
-
: 0;
|
|
2193
|
-
int height = (heightParam != null && heightParam > 0)
|
|
2194
|
-
? (int) (heightParam * pixelRatio)
|
|
2195
|
-
: 0;
|
|
2196
|
-
|
|
2197
|
-
cameraXView.setPreviewSize(x, y, width, height, () -> {
|
|
2198
|
-
// Return the actual preview bounds after layout operations are complete
|
|
2199
|
-
int[] bounds = cameraXView.getCurrentPreviewBounds();
|
|
2200
|
-
JSObject ret = new JSObject();
|
|
2201
|
-
ret.put("x", bounds[0]);
|
|
2202
|
-
ret.put("y", bounds[1]);
|
|
2203
|
-
ret.put("width", bounds[2]);
|
|
2204
|
-
ret.put("height", bounds[3]);
|
|
2205
|
-
call.resolve(ret);
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2208
|
-
|
|
2209
|
-
@PluginMethod
|
|
2210
|
-
public void deleteFile(PluginCall call) {
|
|
2211
|
-
String path = call.getString("path");
|
|
2212
|
-
if (path == null || path.isEmpty()) {
|
|
2213
|
-
call.reject("path parameter is required");
|
|
2214
|
-
return;
|
|
2215
|
-
}
|
|
2216
|
-
try {
|
|
2217
|
-
java.io.File f = new java.io.File(
|
|
2218
|
-
Objects.requireNonNull(Uri.parse(path).getPath())
|
|
2219
|
-
);
|
|
2220
|
-
boolean deleted = f.exists() && f.delete();
|
|
2221
|
-
JSObject ret = new JSObject();
|
|
2222
|
-
ret.put("success", deleted);
|
|
2223
|
-
call.resolve(ret);
|
|
2224
|
-
} catch (Exception e) {
|
|
2225
|
-
call.reject("Failed to delete file: " + e.getMessage());
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
@PluginMethod
|
|
2230
|
-
public void startRecordVideo(PluginCall call) {
|
|
2231
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
2232
|
-
call.reject("Camera is not running");
|
|
2233
|
-
return;
|
|
2234
|
-
}
|
|
2235
|
-
|
|
2236
|
-
boolean disableAudio = call.getBoolean("disableAudio") != null
|
|
2237
|
-
? Boolean.TRUE.equals(call.getBoolean("disableAudio"))
|
|
2238
|
-
: this.lastDisableAudio;
|
|
2239
|
-
this.lastDisableAudio = disableAudio;
|
|
2240
|
-
String permissionAlias = disableAudio
|
|
2241
|
-
? CAMERA_ONLY_PERMISSION_ALIAS
|
|
2242
|
-
: CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
|
|
2243
|
-
|
|
2244
|
-
if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
|
|
2245
|
-
try {
|
|
2246
|
-
cameraXView.startRecordVideo();
|
|
2247
|
-
call.resolve();
|
|
2248
|
-
} catch (Exception e) {
|
|
2249
|
-
call.reject("Failed to start video recording: " + e.getMessage());
|
|
2250
|
-
}
|
|
2251
|
-
} else {
|
|
2252
|
-
requestPermissionForAlias(
|
|
2253
|
-
permissionAlias,
|
|
2254
|
-
call,
|
|
2255
|
-
"handleVideoRecordingPermissionResult"
|
|
2256
|
-
);
|
|
2257
|
-
}
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
@PluginMethod
|
|
2261
|
-
public void stopRecordVideo(PluginCall call) {
|
|
2262
|
-
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
2263
|
-
call.reject("Camera is not running");
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
try {
|
|
2268
|
-
bridge.saveCall(call);
|
|
2269
|
-
final String cbId = call.getCallbackId();
|
|
2270
|
-
cameraXView.stopRecordVideo(
|
|
2271
|
-
new CameraXView.VideoRecordingCallback() {
|
|
2272
|
-
@Override
|
|
2273
|
-
public void onSuccess(String filePath) {
|
|
2274
|
-
PluginCall saved = bridge.getSavedCall(cbId);
|
|
2275
|
-
if (saved != null) {
|
|
2276
|
-
JSObject result = new JSObject();
|
|
2277
|
-
result.put("videoFilePath", filePath);
|
|
2278
|
-
saved.resolve(result);
|
|
2279
|
-
bridge.releaseCall(saved);
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
@Override
|
|
2284
|
-
public void onError(String message) {
|
|
2285
|
-
PluginCall saved = bridge.getSavedCall(cbId);
|
|
2286
|
-
if (saved != null) {
|
|
2287
|
-
saved.reject("Failed to stop video recording: " + message);
|
|
2288
|
-
bridge.releaseCall(saved);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
@PluginMethod
|
|
1778
|
+
public void deleteFile(PluginCall call) {
|
|
1779
|
+
String path = call.getString("path");
|
|
1780
|
+
if (path == null || path.isEmpty()) {
|
|
1781
|
+
call.reject("path parameter is required");
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
try {
|
|
1785
|
+
java.io.File f = new java.io.File(Objects.requireNonNull(Uri.parse(path).getPath()));
|
|
1786
|
+
boolean deleted = f.exists() && f.delete();
|
|
1787
|
+
JSObject ret = new JSObject();
|
|
1788
|
+
ret.put("success", deleted);
|
|
1789
|
+
call.resolve(ret);
|
|
1790
|
+
} catch (Exception e) {
|
|
1791
|
+
call.reject("Failed to delete file: " + e.getMessage());
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
@PluginMethod
|
|
1796
|
+
public void startRecordVideo(PluginCall call) {
|
|
1797
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
1798
|
+
call.reject("Camera is not running");
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
boolean disableAudio = call.getBoolean("disableAudio") != null
|
|
1803
|
+
? Boolean.TRUE.equals(call.getBoolean("disableAudio"))
|
|
1804
|
+
: this.lastDisableAudio;
|
|
1805
|
+
this.lastDisableAudio = disableAudio;
|
|
1806
|
+
String permissionAlias = disableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
|
|
1807
|
+
|
|
1808
|
+
if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
|
|
1809
|
+
try {
|
|
1810
|
+
cameraXView.startRecordVideo();
|
|
1811
|
+
call.resolve();
|
|
1812
|
+
} catch (Exception e) {
|
|
1813
|
+
call.reject("Failed to start video recording: " + e.getMessage());
|
|
2289
1814
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
String permissionAlias = this.lastDisableAudio
|
|
2302
|
-
? CAMERA_ONLY_PERMISSION_ALIAS
|
|
2303
|
-
: CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
|
|
2304
|
-
|
|
2305
|
-
// Check if either permission is granted (mirroring handleCameraPermissionResult)
|
|
2306
|
-
if (
|
|
2307
|
-
PermissionState.GRANTED.equals(
|
|
2308
|
-
getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
|
|
2309
|
-
) ||
|
|
2310
|
-
PermissionState.GRANTED.equals(
|
|
2311
|
-
getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS)
|
|
2312
|
-
)
|
|
2313
|
-
) {
|
|
2314
|
-
try {
|
|
2315
|
-
cameraXView.startRecordVideo();
|
|
2316
|
-
call.resolve();
|
|
2317
|
-
} catch (Exception e) {
|
|
2318
|
-
call.reject("Failed to start video recording: " + e.getMessage());
|
|
2319
|
-
}
|
|
2320
|
-
} else {
|
|
2321
|
-
call.reject(
|
|
2322
|
-
"camera permission denied. enable camera access in Settings.",
|
|
2323
|
-
"cameraPermissionDenied"
|
|
2324
|
-
);
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
|
|
2328
|
-
@PluginMethod
|
|
2329
|
-
public void getSafeAreaInsets(PluginCall call) {
|
|
2330
|
-
JSObject ret = new JSObject();
|
|
2331
|
-
int orientation = getContext()
|
|
2332
|
-
.getResources()
|
|
2333
|
-
.getConfiguration()
|
|
2334
|
-
.orientation;
|
|
2335
|
-
|
|
2336
|
-
int notchInsetPx = 0;
|
|
2337
|
-
|
|
2338
|
-
try {
|
|
2339
|
-
View decorView = getBridge().getActivity().getWindow().getDecorView();
|
|
2340
|
-
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
|
|
2341
|
-
|
|
2342
|
-
if (insets != null) {
|
|
2343
|
-
// Get display cutout insets (notch, punch hole, etc.)
|
|
2344
|
-
// this.Capacitor.Plugins.CameraPreview.getSafeAreaInsets()
|
|
2345
|
-
Insets cutout = insets.getInsets(
|
|
2346
|
-
WindowInsetsCompat.Type.displayCutout()
|
|
2347
|
-
);
|
|
1815
|
+
} else {
|
|
1816
|
+
requestPermissionForAlias(permissionAlias, call, "handleVideoRecordingPermissionResult");
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
@PluginMethod
|
|
1821
|
+
public void stopRecordVideo(PluginCall call) {
|
|
1822
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
1823
|
+
call.reject("Camera is not running");
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
2348
1826
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
1827
|
+
try {
|
|
1828
|
+
bridge.saveCall(call);
|
|
1829
|
+
final String cbId = call.getCallbackId();
|
|
1830
|
+
cameraXView.stopRecordVideo(
|
|
1831
|
+
new CameraXView.VideoRecordingCallback() {
|
|
1832
|
+
@Override
|
|
1833
|
+
public void onSuccess(String filePath) {
|
|
1834
|
+
PluginCall saved = bridge.getSavedCall(cbId);
|
|
1835
|
+
if (saved != null) {
|
|
1836
|
+
JSObject result = new JSObject();
|
|
1837
|
+
result.put("videoFilePath", filePath);
|
|
1838
|
+
saved.resolve(result);
|
|
1839
|
+
bridge.releaseCall(saved);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
@Override
|
|
1844
|
+
public void onError(String message) {
|
|
1845
|
+
PluginCall saved = bridge.getSavedCall(cbId);
|
|
1846
|
+
if (saved != null) {
|
|
1847
|
+
saved.reject("Failed to stop video recording: " + message);
|
|
1848
|
+
bridge.releaseCall(saved);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
);
|
|
1853
|
+
} catch (Exception e) {
|
|
1854
|
+
call.reject("Failed to stop video recording: " + e.getMessage());
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
@PermissionCallback
|
|
1859
|
+
private void handleVideoRecordingPermissionResult(PluginCall call) {
|
|
1860
|
+
// Use the persisted session value to determine which permission we requested
|
|
1861
|
+
String permissionAlias = this.lastDisableAudio ? CAMERA_ONLY_PERMISSION_ALIAS : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
|
|
1862
|
+
|
|
1863
|
+
// Check if either permission is granted (mirroring handleCameraPermissionResult)
|
|
1864
|
+
if (
|
|
1865
|
+
PermissionState.GRANTED.equals(getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)) ||
|
|
1866
|
+
PermissionState.GRANTED.equals(getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS))
|
|
1867
|
+
) {
|
|
1868
|
+
try {
|
|
1869
|
+
cameraXView.startRecordVideo();
|
|
1870
|
+
call.resolve();
|
|
1871
|
+
} catch (Exception e) {
|
|
1872
|
+
call.reject("Failed to start video recording: " + e.getMessage());
|
|
1873
|
+
}
|
|
2365
1874
|
} else {
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
1875
|
+
call.reject("camera permission denied. enable camera access in Settings.", "cameraPermissionDenied");
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
@PluginMethod
|
|
1880
|
+
public void getSafeAreaInsets(PluginCall call) {
|
|
1881
|
+
JSObject ret = new JSObject();
|
|
1882
|
+
int orientation = getContext().getResources().getConfiguration().orientation;
|
|
1883
|
+
|
|
1884
|
+
int notchInsetPx = 0;
|
|
1885
|
+
|
|
1886
|
+
try {
|
|
1887
|
+
View decorView = getBridge().getActivity().getWindow().getDecorView();
|
|
1888
|
+
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(decorView);
|
|
1889
|
+
|
|
1890
|
+
if (insets != null) {
|
|
1891
|
+
// Get display cutout insets (notch, punch hole, etc.)
|
|
1892
|
+
// this.Capacitor.Plugins.CameraPreview.getSafeAreaInsets()
|
|
1893
|
+
Insets cutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout());
|
|
1894
|
+
|
|
1895
|
+
// Get system bars insets (status bar, navigation bars)
|
|
1896
|
+
Insets sysBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
1897
|
+
|
|
1898
|
+
// In portrait mode, notch is at the top
|
|
1899
|
+
// In landscape mode, notch is typically at the left side (or right, but left is more common)
|
|
1900
|
+
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
|
|
1901
|
+
// Portrait: return top inset (notch/status bar)
|
|
1902
|
+
notchInsetPx = Math.max(cutout.top, sysBars.top);
|
|
1903
|
+
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
1904
|
+
// Landscape: return left inset (notch moved to side)
|
|
1905
|
+
notchInsetPx = Math.max(cutout.left, sysBars.left);
|
|
1906
|
+
// Additional fallback: some devices might have the notch on the right in landscape
|
|
1907
|
+
// If left is 0, check right side as well
|
|
1908
|
+
if (notchInsetPx == 0) {
|
|
1909
|
+
notchInsetPx = Math.max(cutout.right, sysBars.right);
|
|
1910
|
+
}
|
|
1911
|
+
} else {
|
|
1912
|
+
// Unknown orientation, default to top
|
|
1913
|
+
notchInsetPx = Math.max(cutout.top, sysBars.top);
|
|
1914
|
+
}
|
|
1915
|
+
} else {
|
|
1916
|
+
// Fallback to status bar height if WindowInsets are not available
|
|
1917
|
+
notchInsetPx = getStatusBarHeightPx();
|
|
1918
|
+
}
|
|
1919
|
+
} catch (Exception e) {
|
|
1920
|
+
// Final fallback
|
|
1921
|
+
notchInsetPx = getStatusBarHeightPx();
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// Convert pixels to dp for consistency with JS layout units
|
|
1925
|
+
float density = getContext().getResources().getDisplayMetrics().density;
|
|
1926
|
+
ret.put("orientation", orientation);
|
|
1927
|
+
ret.put("top", notchInsetPx / density);
|
|
1928
|
+
call.resolve(ret);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
private int getStatusBarHeightPx() {
|
|
1932
|
+
int result = 0;
|
|
1933
|
+
@SuppressLint("InternalInsetResource")
|
|
1934
|
+
int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
|
|
1935
|
+
if (resourceId > 0) {
|
|
1936
|
+
result = getContext().getResources().getDimensionPixelSize(resourceId);
|
|
1937
|
+
}
|
|
1938
|
+
return result;
|
|
1939
|
+
}
|
|
2396
1940
|
}
|