@capgo/camera-preview 8.4.2 → 8.4.4
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/android/src/main/java/app/capgo/capacitor/camera/preview/CameraPreview.java +397 -67
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java +149 -69
- package/android/src/main/java/app/capgo/capacitor/camera/preview/model/CameraSessionConfiguration.java +9 -0
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +1 -1
- package/package.json +1 -1
|
@@ -6,7 +6,11 @@ import static android.Manifest.permission.RECORD_AUDIO;
|
|
|
6
6
|
import android.Manifest;
|
|
7
7
|
import android.annotation.SuppressLint;
|
|
8
8
|
import android.app.Activity;
|
|
9
|
+
import android.app.KeyguardManager;
|
|
10
|
+
import android.content.BroadcastReceiver;
|
|
11
|
+
import android.content.Context;
|
|
9
12
|
import android.content.Intent;
|
|
13
|
+
import android.content.IntentFilter;
|
|
10
14
|
import android.content.pm.ActivityInfo;
|
|
11
15
|
import android.content.pm.PackageManager;
|
|
12
16
|
import android.content.res.Configuration;
|
|
@@ -69,12 +73,19 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
69
73
|
|
|
70
74
|
private final String pluginVersion = "";
|
|
71
75
|
|
|
76
|
+
@Override
|
|
77
|
+
public void load() {
|
|
78
|
+
registerScreenLockReceiver();
|
|
79
|
+
}
|
|
80
|
+
|
|
72
81
|
@Override
|
|
73
82
|
protected void handleOnPause() {
|
|
74
83
|
super.handleOnPause();
|
|
84
|
+
activityPaused = true;
|
|
75
85
|
if (cameraXView != null && cameraXView.isRunning()) {
|
|
76
86
|
// Store the current configuration before stopping
|
|
77
87
|
lastSessionConfig = cameraXView.getSessionConfig();
|
|
88
|
+
requestBarcodeScannerRestartAfterCameraResume();
|
|
78
89
|
cameraXView.stopSession();
|
|
79
90
|
}
|
|
80
91
|
}
|
|
@@ -82,22 +93,25 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
82
93
|
@Override
|
|
83
94
|
protected void handleOnResume() {
|
|
84
95
|
super.handleOnResume();
|
|
96
|
+
registerScreenLockReceiver();
|
|
97
|
+
activityPaused = false;
|
|
85
98
|
if (lastSessionConfig != null) {
|
|
86
|
-
// Set to black to avoid flicker, transparent set later
|
|
87
|
-
if (lastSessionConfig.isToBack()) {
|
|
88
|
-
try {
|
|
89
|
-
getBridge()
|
|
90
|
-
.getActivity()
|
|
91
|
-
.getWindow()
|
|
92
|
-
.setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.BLACK));
|
|
93
|
-
getBridge().getWebView().setBackgroundColor(android.graphics.Color.BLACK);
|
|
94
|
-
} catch (Exception ignored) {}
|
|
95
|
-
}
|
|
96
99
|
// Recreate camera with last known configuration
|
|
97
|
-
if (cameraXView == null) {
|
|
100
|
+
if (cameraXView == null || !cameraXView.isRunning() || cameraXView.isStopping()) {
|
|
98
101
|
cameraXView = new CameraXView(getContext(), getBridge().getWebView());
|
|
99
102
|
cameraXView.setListener(this);
|
|
100
103
|
}
|
|
104
|
+
if (lastSessionConfig.isToBack()) {
|
|
105
|
+
if (usesFullStackTransparentBackgroundWorkaround()) {
|
|
106
|
+
activateTransparentBackgroundsForToBack(cameraXView);
|
|
107
|
+
} else {
|
|
108
|
+
prepareTransparentBackgroundsForToBack(cameraXView);
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
toBackVisualStateActive = false;
|
|
112
|
+
}
|
|
113
|
+
requestBarcodeScannerRestartAfterCameraResume();
|
|
114
|
+
cameraRestartAfterResumeInProgress = true;
|
|
101
115
|
cameraXView.startSession(lastSessionConfig);
|
|
102
116
|
}
|
|
103
117
|
}
|
|
@@ -110,12 +124,18 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
110
124
|
cameraXView = null;
|
|
111
125
|
}
|
|
112
126
|
lastSessionConfig = null;
|
|
127
|
+
clearActiveBarcodeScanner();
|
|
128
|
+
toBackVisualStateActive = false;
|
|
129
|
+
restoreOriginalWindowBackground(getBridge().getActivity());
|
|
130
|
+
restoreWebViewVisualState();
|
|
113
131
|
restoreSystemUiForToBackMode(getBridge().getActivity());
|
|
132
|
+
unregisterScreenLockReceiver();
|
|
114
133
|
}
|
|
115
134
|
|
|
116
135
|
private CameraSessionConfiguration lastSessionConfig;
|
|
117
136
|
|
|
118
137
|
private static final String TAG = "CameraPreview CameraXView";
|
|
138
|
+
private static final int DEFAULT_WEB_VIEW_BACKGROUND = Color.WHITE;
|
|
119
139
|
|
|
120
140
|
static final String CAMERA_WITH_AUDIO_PERMISSION_ALIAS = "cameraWithAudio";
|
|
121
141
|
static final String CAMERA_ONLY_PERMISSION_ALIAS = "cameraOnly";
|
|
@@ -138,15 +158,40 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
138
158
|
private boolean lastDisableAudio = true;
|
|
139
159
|
private boolean lastIncludeSafeAreaInsets = false;
|
|
140
160
|
private Drawable originalWindowBackground;
|
|
161
|
+
private boolean originalWindowBackgroundCaptured = false;
|
|
162
|
+
private Drawable originalWebViewBackground;
|
|
163
|
+
private boolean originalWebViewBackgroundCaptured = false;
|
|
141
164
|
private Float originalWebViewAlpha;
|
|
142
165
|
private Drawable originalWebViewParentBackground;
|
|
166
|
+
private boolean originalWebViewParentBackgroundCaptured = false;
|
|
143
167
|
private Integer originalStatusBarColor;
|
|
144
168
|
private Integer originalNavigationBarColor;
|
|
145
169
|
private Boolean originalNavigationBarContrastEnforced;
|
|
170
|
+
private volatile boolean toBackVisualStateActive = false;
|
|
146
171
|
private boolean isCameraPermissionDialogShowing = false;
|
|
147
172
|
private boolean pendingStartBarcodeScanner = false;
|
|
148
173
|
private List<String> pendingStartBarcodeFormats = new ArrayList<>();
|
|
149
174
|
private int pendingStartBarcodeDetectionInterval = 500;
|
|
175
|
+
private final Object activeBarcodeScannerLock = new Object();
|
|
176
|
+
private boolean activeBarcodeScanner = false;
|
|
177
|
+
private List<String> activeBarcodeFormats = new ArrayList<>();
|
|
178
|
+
private int activeBarcodeDetectionInterval = 500;
|
|
179
|
+
private boolean restartBarcodeScannerAfterCameraResume = false;
|
|
180
|
+
private boolean screenLocked = false;
|
|
181
|
+
private boolean activityPaused = false;
|
|
182
|
+
private boolean cameraRestartAfterResumeInProgress = false;
|
|
183
|
+
private BroadcastReceiver screenLockReceiver;
|
|
184
|
+
|
|
185
|
+
private static final class BarcodeScannerRequest {
|
|
186
|
+
|
|
187
|
+
private final List<String> formats;
|
|
188
|
+
private final int detectionInterval;
|
|
189
|
+
|
|
190
|
+
private BarcodeScannerRequest(List<String> formats, int detectionInterval) {
|
|
191
|
+
this.formats = formats;
|
|
192
|
+
this.detectionInterval = detectionInterval;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
150
195
|
|
|
151
196
|
@PluginMethod
|
|
152
197
|
public void getExposureModes(PluginCall call) {
|
|
@@ -418,6 +463,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
418
463
|
new CameraXView.BarcodeScannerStartCallback() {
|
|
419
464
|
@Override
|
|
420
465
|
public void onStarted() {
|
|
466
|
+
markActiveBarcodeScanner(formats, detectionInterval != null ? detectionInterval : 500);
|
|
421
467
|
call.resolve();
|
|
422
468
|
}
|
|
423
469
|
|
|
@@ -431,6 +477,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
431
477
|
|
|
432
478
|
@PluginMethod
|
|
433
479
|
public void stopBarcodeScanner(PluginCall call) {
|
|
480
|
+
clearActiveBarcodeScanner();
|
|
434
481
|
if (cameraXView != null) {
|
|
435
482
|
cameraXView.stopBarcodeScanner();
|
|
436
483
|
}
|
|
@@ -492,6 +539,176 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
492
539
|
pendingStartBarcodeDetectionInterval = 500;
|
|
493
540
|
}
|
|
494
541
|
|
|
542
|
+
private void markActiveBarcodeScanner(List<String> formats, int detectionInterval) {
|
|
543
|
+
synchronized (activeBarcodeScannerLock) {
|
|
544
|
+
activeBarcodeScanner = true;
|
|
545
|
+
activeBarcodeFormats = new ArrayList<>(formats);
|
|
546
|
+
activeBarcodeDetectionInterval = detectionInterval;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private void clearActiveBarcodeScanner() {
|
|
551
|
+
synchronized (activeBarcodeScannerLock) {
|
|
552
|
+
activeBarcodeScanner = false;
|
|
553
|
+
activeBarcodeFormats = new ArrayList<>();
|
|
554
|
+
activeBarcodeDetectionInterval = 500;
|
|
555
|
+
restartBarcodeScannerAfterCameraResume = false;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private void requestBarcodeScannerRestartAfterCameraResume() {
|
|
560
|
+
synchronized (activeBarcodeScannerLock) {
|
|
561
|
+
if (activeBarcodeScanner) {
|
|
562
|
+
restartBarcodeScannerAfterCameraResume = true;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private BarcodeScannerRequest consumeBarcodeScannerRestartRequest() {
|
|
568
|
+
synchronized (activeBarcodeScannerLock) {
|
|
569
|
+
if (!activeBarcodeScanner || !restartBarcodeScannerAfterCameraResume) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
restartBarcodeScannerAfterCameraResume = false;
|
|
573
|
+
return new BarcodeScannerRequest(new ArrayList<>(activeBarcodeFormats), activeBarcodeDetectionInterval);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private void restoreBarcodeScannerRestartRequest(BarcodeScannerRequest request) {
|
|
578
|
+
if (request == null) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
synchronized (activeBarcodeScannerLock) {
|
|
582
|
+
if (activeBarcodeScanner) {
|
|
583
|
+
restartBarcodeScannerAfterCameraResume = true;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
private void restartBarcodeScannerAfterCameraResumeIfNeeded() {
|
|
589
|
+
if (screenLocked || isDeviceLocked()) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (cameraXView == null || !cameraXView.isRunning()) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
BarcodeScannerRequest request = consumeBarcodeScannerRestartRequest();
|
|
598
|
+
if (request == null) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
cameraXView.startBarcodeScanner(
|
|
603
|
+
request.formats,
|
|
604
|
+
request.detectionInterval,
|
|
605
|
+
new CameraXView.BarcodeScannerStartCallback() {
|
|
606
|
+
@Override
|
|
607
|
+
public void onStarted() {
|
|
608
|
+
markActiveBarcodeScanner(request.formats, request.detectionInterval);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
@Override
|
|
612
|
+
public void onError(String message) {
|
|
613
|
+
restoreBarcodeScannerRestartRequest(request);
|
|
614
|
+
onBarcodeScanError("Failed to restart barcode scanner after unlock: " + message);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
private void registerScreenLockReceiver() {
|
|
621
|
+
if (screenLockReceiver != null) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
screenLockReceiver = new BroadcastReceiver() {
|
|
626
|
+
@Override
|
|
627
|
+
public void onReceive(Context context, Intent intent) {
|
|
628
|
+
String action = intent != null ? intent.getAction() : null;
|
|
629
|
+
if (Intent.ACTION_SCREEN_OFF.equals(action)) {
|
|
630
|
+
handleScreenOff();
|
|
631
|
+
} else if (Intent.ACTION_SCREEN_ON.equals(action)) {
|
|
632
|
+
handleScreenOn();
|
|
633
|
+
} else if (Intent.ACTION_USER_PRESENT.equals(action)) {
|
|
634
|
+
handleScreenUnlocked();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
IntentFilter filter = new IntentFilter();
|
|
640
|
+
filter.addAction(Intent.ACTION_SCREEN_OFF);
|
|
641
|
+
filter.addAction(Intent.ACTION_SCREEN_ON);
|
|
642
|
+
filter.addAction(Intent.ACTION_USER_PRESENT);
|
|
643
|
+
|
|
644
|
+
Context context = getContext();
|
|
645
|
+
if (context == null) {
|
|
646
|
+
screenLockReceiver = null;
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
650
|
+
context.registerReceiver(screenLockReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
|
651
|
+
} else {
|
|
652
|
+
context.registerReceiver(screenLockReceiver, filter);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
private void unregisterScreenLockReceiver() {
|
|
657
|
+
if (screenLockReceiver == null) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
Context context = getContext();
|
|
662
|
+
if (context != null) {
|
|
663
|
+
context.unregisterReceiver(screenLockReceiver);
|
|
664
|
+
}
|
|
665
|
+
} catch (IllegalArgumentException ignored) {
|
|
666
|
+
// Receiver may already be unregistered by the host activity.
|
|
667
|
+
} finally {
|
|
668
|
+
screenLockReceiver = null;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private boolean isDeviceLocked() {
|
|
673
|
+
Context context = getContext();
|
|
674
|
+
KeyguardManager keyguardManager = context != null ? (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE) : null;
|
|
675
|
+
return keyguardManager != null && keyguardManager.isKeyguardLocked();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private void handleScreenOff() {
|
|
679
|
+
screenLocked = true;
|
|
680
|
+
requestBarcodeScannerRestartAfterCameraResume();
|
|
681
|
+
if (cameraXView != null && cameraXView.isRunning() && !cameraXView.isStopping()) {
|
|
682
|
+
lastSessionConfig = cameraXView.getSessionConfig();
|
|
683
|
+
cameraXView.stopSession();
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private void handleScreenOn() {
|
|
688
|
+
screenLocked = isDeviceLocked();
|
|
689
|
+
if (!screenLocked) {
|
|
690
|
+
restartBarcodeScannerAfterCameraResumeIfNeeded();
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private void handleScreenUnlocked() {
|
|
695
|
+
screenLocked = false;
|
|
696
|
+
if (
|
|
697
|
+
!activityPaused &&
|
|
698
|
+
!cameraRestartAfterResumeInProgress &&
|
|
699
|
+
lastSessionConfig != null &&
|
|
700
|
+
(cameraXView == null || cameraXView.isStopping())
|
|
701
|
+
) {
|
|
702
|
+
cameraXView = new CameraXView(getContext(), getBridge().getWebView());
|
|
703
|
+
cameraXView.setListener(this);
|
|
704
|
+
requestBarcodeScannerRestartAfterCameraResume();
|
|
705
|
+
cameraRestartAfterResumeInProgress = true;
|
|
706
|
+
cameraXView.startSession(lastSessionConfig);
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
restartBarcodeScannerAfterCameraResumeIfNeeded();
|
|
710
|
+
}
|
|
711
|
+
|
|
495
712
|
@PluginMethod
|
|
496
713
|
public void stop(final PluginCall call) {
|
|
497
714
|
boolean force = Boolean.TRUE.equals(call.getBoolean("force", false));
|
|
@@ -524,13 +741,9 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
524
741
|
}
|
|
525
742
|
// Manual stops should not trigger automatic resume with stale config
|
|
526
743
|
lastSessionConfig = null;
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
getBridge().getActivity().getWindow().setBackgroundDrawable(originalWindowBackground);
|
|
531
|
-
} catch (Exception ignored) {}
|
|
532
|
-
originalWindowBackground = null;
|
|
533
|
-
}
|
|
744
|
+
clearActiveBarcodeScanner();
|
|
745
|
+
toBackVisualStateActive = false;
|
|
746
|
+
restoreOriginalWindowBackground(getBridge().getActivity());
|
|
534
747
|
restoreWebViewVisualState();
|
|
535
748
|
restoreSystemUiForToBackMode(getBridge().getActivity());
|
|
536
749
|
call.resolve();
|
|
@@ -1051,16 +1264,14 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1051
1264
|
.getActivity()
|
|
1052
1265
|
.runOnUiThread(() -> {
|
|
1053
1266
|
lockSystemUiForToBackMode(getBridge().getActivity(), toBack);
|
|
1054
|
-
// Ensure transparent background when preview is behind the WebView (Android 10 fix)
|
|
1055
1267
|
if (toBack) {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
} catch (Exception ignored) {}
|
|
1268
|
+
if (usesFullStackTransparentBackgroundWorkaround()) {
|
|
1269
|
+
activateTransparentBackgroundsForToBack(cameraXView);
|
|
1270
|
+
} else {
|
|
1271
|
+
prepareTransparentBackgroundsForToBack(cameraXView);
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
toBackVisualStateActive = false;
|
|
1064
1275
|
}
|
|
1065
1276
|
DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
|
|
1066
1277
|
if (lockOrientation) {
|
|
@@ -1337,7 +1548,11 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1337
1548
|
config.setTargetZoom(finalTargetZoom);
|
|
1338
1549
|
config.setCentered(isCentered);
|
|
1339
1550
|
config.setEnablePhysicalDeviceSelection(enablePhysicalDeviceSelection);
|
|
1551
|
+
config.setBarcodeScannerEnabled(barcodeScannerOptions != null);
|
|
1340
1552
|
setPendingStartBarcodeScanner(barcodeScannerOptions);
|
|
1553
|
+
if (barcodeScannerOptions == null) {
|
|
1554
|
+
clearActiveBarcodeScanner();
|
|
1555
|
+
}
|
|
1341
1556
|
|
|
1342
1557
|
bridge.saveCall(call);
|
|
1343
1558
|
cameraStartCallbackId = call.getCallbackId();
|
|
@@ -1625,11 +1840,14 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1625
1840
|
Log.d(TAG, "onCameraStopped: ignoring callback from stale instance");
|
|
1626
1841
|
return;
|
|
1627
1842
|
}
|
|
1843
|
+
cameraRestartAfterResumeInProgress = false;
|
|
1628
1844
|
// Ensure reference is cleared once the originating CameraXView has fully stopped
|
|
1629
|
-
if (cameraXView == source) {
|
|
1845
|
+
if (source != null && cameraXView == source) {
|
|
1630
1846
|
cameraXView = null;
|
|
1631
1847
|
}
|
|
1632
|
-
|
|
1848
|
+
if (!toBackVisualStateActive) {
|
|
1849
|
+
restoreWebViewVisualState();
|
|
1850
|
+
}
|
|
1633
1851
|
|
|
1634
1852
|
PluginCall queuedCall = null;
|
|
1635
1853
|
synchronized (pendingStartLock) {
|
|
@@ -1676,6 +1894,72 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1676
1894
|
return manufacturer.contains("xiaomi") || brand.contains("xiaomi") || brand.contains("redmi") || brand.contains("poco");
|
|
1677
1895
|
}
|
|
1678
1896
|
|
|
1897
|
+
private boolean usesFullStackTransparentBackgroundWorkaround() {
|
|
1898
|
+
String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER.toLowerCase(Locale.US) : "";
|
|
1899
|
+
String brand = Build.BRAND != null ? Build.BRAND.toLowerCase(Locale.US) : "";
|
|
1900
|
+
return (
|
|
1901
|
+
isMiuiDevice() ||
|
|
1902
|
+
manufacturer.contains("huawei") ||
|
|
1903
|
+
manufacturer.contains("honor") ||
|
|
1904
|
+
brand.contains("huawei") ||
|
|
1905
|
+
brand.contains("honor")
|
|
1906
|
+
);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
private void captureOriginalWindowBackground(Activity activity) {
|
|
1910
|
+
if (activity == null) {
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
synchronized (this) {
|
|
1914
|
+
if (!originalWindowBackgroundCaptured) {
|
|
1915
|
+
originalWindowBackground = activity.getWindow().getDecorView().getBackground();
|
|
1916
|
+
originalWindowBackgroundCaptured = true;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
private void captureOriginalWebViewVisualState(WebView webView, ViewGroup webViewParent) {
|
|
1922
|
+
if (webView == null) {
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
synchronized (this) {
|
|
1926
|
+
if (!originalWebViewBackgroundCaptured) {
|
|
1927
|
+
originalWebViewBackground = webView.getBackground();
|
|
1928
|
+
originalWebViewBackgroundCaptured = true;
|
|
1929
|
+
}
|
|
1930
|
+
if (originalWebViewAlpha == null) {
|
|
1931
|
+
originalWebViewAlpha = webView.getAlpha();
|
|
1932
|
+
}
|
|
1933
|
+
if (webViewParent != null && !originalWebViewParentBackgroundCaptured) {
|
|
1934
|
+
originalWebViewParentBackground = webViewParent.getBackground();
|
|
1935
|
+
originalWebViewParentBackgroundCaptured = true;
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
private void restoreOriginalWindowBackground(Activity activity) {
|
|
1941
|
+
final Drawable backgroundToRestore;
|
|
1942
|
+
final boolean captured;
|
|
1943
|
+
synchronized (this) {
|
|
1944
|
+
backgroundToRestore = originalWindowBackground;
|
|
1945
|
+
captured = originalWindowBackgroundCaptured;
|
|
1946
|
+
originalWindowBackground = null;
|
|
1947
|
+
originalWindowBackgroundCaptured = false;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
if (!captured || activity == null) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
activity.runOnUiThread(() -> {
|
|
1955
|
+
try {
|
|
1956
|
+
activity.getWindow().setBackgroundDrawable(backgroundToRestore);
|
|
1957
|
+
} catch (Exception e) {
|
|
1958
|
+
Log.w(TAG, "Failed to restore window background", e);
|
|
1959
|
+
}
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1679
1963
|
private int toOpaqueColor(int color) {
|
|
1680
1964
|
return Color.argb(255, Color.red(color), Color.green(color), Color.blue(color));
|
|
1681
1965
|
}
|
|
@@ -1741,35 +2025,46 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1741
2025
|
});
|
|
1742
2026
|
}
|
|
1743
2027
|
|
|
1744
|
-
private void
|
|
1745
|
-
if (!isToBackMode()) {
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
2028
|
+
private void prepareTransparentBackgroundsForToBack(CameraXView visualStateOwner) {
|
|
1748
2029
|
Activity activity = getActivity();
|
|
1749
2030
|
WebView webView = getBridge().getWebView();
|
|
1750
2031
|
if (activity == null || webView == null) {
|
|
1751
2032
|
return;
|
|
1752
2033
|
}
|
|
1753
2034
|
|
|
1754
|
-
|
|
1755
|
-
|
|
2035
|
+
toBackVisualStateActive = true;
|
|
2036
|
+
final ViewGroup webViewParent = (ViewGroup) webView.getParent();
|
|
2037
|
+
captureOriginalWindowBackground(activity);
|
|
2038
|
+
captureOriginalWebViewVisualState(webView, webViewParent);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
private void activateTransparentBackgroundsForToBack(CameraXView visualStateOwner) {
|
|
2042
|
+
prepareTransparentBackgroundsForToBack(visualStateOwner);
|
|
2043
|
+
Activity activity = getActivity();
|
|
2044
|
+
WebView webView = getBridge().getWebView();
|
|
2045
|
+
if (activity == null || webView == null) {
|
|
2046
|
+
return;
|
|
1756
2047
|
}
|
|
1757
2048
|
|
|
1758
2049
|
final ViewGroup webViewParent = (ViewGroup) webView.getParent();
|
|
1759
|
-
if (webViewParent != null && originalWebViewParentBackground == null) {
|
|
1760
|
-
originalWebViewParentBackground = webViewParent.getBackground();
|
|
1761
|
-
}
|
|
1762
2050
|
|
|
1763
2051
|
Runnable apply = () -> {
|
|
1764
2052
|
try {
|
|
1765
|
-
|
|
1766
|
-
|
|
2053
|
+
if (!toBackVisualStateActive || visualStateOwner == null || cameraXView != visualStateOwner) {
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
boolean fullStackWorkaround = usesFullStackTransparentBackgroundWorkaround();
|
|
2057
|
+
if (fullStackWorkaround) {
|
|
2058
|
+
activity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
|
2059
|
+
}
|
|
2060
|
+
if (webViewParent != null && fullStackWorkaround) {
|
|
1767
2061
|
webViewParent.setBackgroundColor(Color.TRANSPARENT);
|
|
1768
2062
|
}
|
|
1769
|
-
|
|
1770
|
-
// fully transparent layers as black.
|
|
1771
|
-
webView.setBackgroundColor(Color.argb(1, 255, 255, 255));
|
|
2063
|
+
webView.setBackgroundColor(isMiuiDevice() ? Color.argb(1, 255, 255, 255) : Color.TRANSPARENT);
|
|
1772
2064
|
webView.setAlpha(isMiuiDevice() ? 0.99f : (originalWebViewAlpha != null ? originalWebViewAlpha : 1f));
|
|
2065
|
+
if (webViewParent != null) {
|
|
2066
|
+
webViewParent.requestTransparentRegion(webView);
|
|
2067
|
+
}
|
|
1773
2068
|
} catch (Exception e) {
|
|
1774
2069
|
Log.w(TAG, "Failed to set backgrounds to transparent", e);
|
|
1775
2070
|
}
|
|
@@ -1784,13 +2079,41 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1784
2079
|
});
|
|
1785
2080
|
}
|
|
1786
2081
|
|
|
2082
|
+
private void applyTransparentBackgroundsForToBack() {
|
|
2083
|
+
if (!isToBackMode()) {
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
activateTransparentBackgroundsForToBack(cameraXView);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1787
2089
|
private void restoreWebViewVisualState() {
|
|
2090
|
+
if (toBackVisualStateActive) {
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
1788
2094
|
Activity activity = getActivity();
|
|
1789
2095
|
WebView webView = getBridge().getWebView();
|
|
1790
|
-
final Float alphaToRestore
|
|
1791
|
-
final Drawable
|
|
1792
|
-
|
|
1793
|
-
|
|
2096
|
+
final Float alphaToRestore;
|
|
2097
|
+
final Drawable webViewBackground;
|
|
2098
|
+
final boolean webViewBackgroundCaptured;
|
|
2099
|
+
final Drawable parentBackground;
|
|
2100
|
+
final boolean parentBackgroundCaptured;
|
|
2101
|
+
synchronized (this) {
|
|
2102
|
+
alphaToRestore = originalWebViewAlpha;
|
|
2103
|
+
webViewBackground = originalWebViewBackground;
|
|
2104
|
+
webViewBackgroundCaptured = originalWebViewBackgroundCaptured;
|
|
2105
|
+
parentBackground = originalWebViewParentBackground;
|
|
2106
|
+
parentBackgroundCaptured = originalWebViewParentBackgroundCaptured;
|
|
2107
|
+
originalWebViewAlpha = null;
|
|
2108
|
+
originalWebViewBackground = null;
|
|
2109
|
+
originalWebViewBackgroundCaptured = false;
|
|
2110
|
+
originalWebViewParentBackground = null;
|
|
2111
|
+
originalWebViewParentBackgroundCaptured = false;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
if (alphaToRestore == null && !webViewBackgroundCaptured && !parentBackgroundCaptured) {
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
1794
2117
|
|
|
1795
2118
|
if (activity == null || webView == null) {
|
|
1796
2119
|
return;
|
|
@@ -1802,7 +2125,12 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1802
2125
|
if (alphaToRestore != null) {
|
|
1803
2126
|
webView.setAlpha(alphaToRestore);
|
|
1804
2127
|
}
|
|
1805
|
-
if (
|
|
2128
|
+
if (webViewBackgroundCaptured) {
|
|
2129
|
+
webView.setBackground(webViewBackground);
|
|
2130
|
+
} else {
|
|
2131
|
+
webView.setBackgroundColor(DEFAULT_WEB_VIEW_BACKGROUND);
|
|
2132
|
+
}
|
|
2133
|
+
if (webViewParent != null && parentBackgroundCaptured) {
|
|
1806
2134
|
webViewParent.setBackground(parentBackground);
|
|
1807
2135
|
}
|
|
1808
2136
|
} catch (Exception ignored) {}
|
|
@@ -1811,6 +2139,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1811
2139
|
|
|
1812
2140
|
@Override
|
|
1813
2141
|
public void onCameraStarted(int width, int height, int x, int y) {
|
|
2142
|
+
cameraRestartAfterResumeInProgress = false;
|
|
1814
2143
|
// Always transition window and WebView backgrounds to transparent when the camera starts,
|
|
1815
2144
|
// regardless of whether there is a pending JS call. This is critical for the
|
|
1816
2145
|
// background/foreground resume cycle: on resume, handleOnResume() sets backgrounds to
|
|
@@ -1971,6 +2300,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1971
2300
|
new CameraXView.BarcodeScannerStartCallback() {
|
|
1972
2301
|
@Override
|
|
1973
2302
|
public void onStarted() {
|
|
2303
|
+
markActiveBarcodeScanner(formats, detectionInterval);
|
|
1974
2304
|
resolveCameraStartCall(call, result);
|
|
1975
2305
|
}
|
|
1976
2306
|
|
|
@@ -1985,6 +2315,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1985
2315
|
|
|
1986
2316
|
resolveCameraStartCall(call, result);
|
|
1987
2317
|
}
|
|
2318
|
+
restartBarcodeScannerAfterCameraResumeIfNeeded();
|
|
1988
2319
|
}
|
|
1989
2320
|
|
|
1990
2321
|
private void resolveCameraStartCall(PluginCall call, JSObject result) {
|
|
@@ -2042,7 +2373,23 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
2042
2373
|
}
|
|
2043
2374
|
|
|
2044
2375
|
@Override
|
|
2045
|
-
public void onCameraStartError(String message) {
|
|
2376
|
+
public void onCameraStartError(CameraXView source, String message) {
|
|
2377
|
+
if (cameraXView != null && cameraXView != source) {
|
|
2378
|
+
Log.d(TAG, "onCameraStartError: ignoring callback from stale instance");
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
cameraRestartAfterResumeInProgress = false;
|
|
2382
|
+
toBackVisualStateActive = false;
|
|
2383
|
+
if (cameraXView == source) {
|
|
2384
|
+
try {
|
|
2385
|
+
// Keep the reference until onCameraStopped clears it after native cleanup.
|
|
2386
|
+
source.stopSession();
|
|
2387
|
+
} catch (Exception e) {
|
|
2388
|
+
Log.w(TAG, "onCameraStartError: failed to stop failed camera session", e);
|
|
2389
|
+
cameraXView = null;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2046
2393
|
PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
|
|
2047
2394
|
if (call != null) {
|
|
2048
2395
|
call.reject(message);
|
|
@@ -2051,24 +2398,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
2051
2398
|
resetPendingStartBarcodeScanner();
|
|
2052
2399
|
}
|
|
2053
2400
|
|
|
2054
|
-
|
|
2055
|
-
// Use synchronized block to ensure only one thread captures and clears the background.
|
|
2056
|
-
// Even if multiple errors occur, only the first will have a non-null background to restore.
|
|
2057
|
-
synchronized (this) {
|
|
2058
|
-
final Drawable backgroundToRestore = originalWindowBackground;
|
|
2059
|
-
if (backgroundToRestore != null) {
|
|
2060
|
-
originalWindowBackground = null; // Clear immediately so other threads won't restore
|
|
2061
|
-
getBridge()
|
|
2062
|
-
.getActivity()
|
|
2063
|
-
.runOnUiThread(() -> {
|
|
2064
|
-
try {
|
|
2065
|
-
getBridge().getActivity().getWindow().setBackgroundDrawable(backgroundToRestore);
|
|
2066
|
-
} catch (Exception e) {
|
|
2067
|
-
Log.w(TAG, "Failed to restore window background on error", e);
|
|
2068
|
-
}
|
|
2069
|
-
});
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2401
|
+
restoreOriginalWindowBackground(getBridge().getActivity());
|
|
2072
2402
|
restoreWebViewVisualState();
|
|
2073
2403
|
restoreSystemUiForToBackMode(getBridge().getActivity());
|
|
2074
2404
|
}
|
|
@@ -129,7 +129,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
129
129
|
void onBarcodesScanned(JSONArray barcodes);
|
|
130
130
|
void onBarcodeScanError(String message);
|
|
131
131
|
void onCameraStarted(int width, int height, int x, int y);
|
|
132
|
-
void onCameraStartError(String message);
|
|
132
|
+
void onCameraStartError(CameraXView source, String message);
|
|
133
133
|
void onCameraStopped(CameraXView source);
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -191,6 +191,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
191
191
|
private volatile boolean isBarcodeFrameProcessing = false;
|
|
192
192
|
private volatile long lastBarcodeFrameAtMs = 0L;
|
|
193
193
|
private volatile long barcodeDetectionIntervalMs = 500L;
|
|
194
|
+
private boolean cameraStartedCallbackSent = false;
|
|
194
195
|
|
|
195
196
|
// Operation coordination (acts like a semaphore to prevent stop during active ops)
|
|
196
197
|
private final Object operationLock = new Object();
|
|
@@ -481,13 +482,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
481
482
|
// runnable is still queued — never transition backward from DESTROYED.
|
|
482
483
|
if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
483
484
|
if (listener != null) {
|
|
484
|
-
listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
|
|
485
|
+
listener.onCameraStartError(this, "Camera start aborted: lifecycle destroyed");
|
|
485
486
|
}
|
|
486
487
|
return;
|
|
487
488
|
}
|
|
488
489
|
if (stopRequested) {
|
|
489
490
|
if (listener != null) {
|
|
490
|
-
listener.onCameraStartError("Camera start aborted: stop requested");
|
|
491
|
+
listener.onCameraStartError(this, "Camera start aborted: stop requested");
|
|
491
492
|
}
|
|
492
493
|
return;
|
|
493
494
|
}
|
|
@@ -496,44 +497,45 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
496
497
|
lifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
|
|
497
498
|
if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
498
499
|
if (listener != null) {
|
|
499
|
-
listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
|
|
500
|
+
listener.onCameraStartError(this, "Camera start aborted: lifecycle destroyed");
|
|
500
501
|
}
|
|
501
502
|
return;
|
|
502
503
|
}
|
|
503
504
|
if (stopRequested) {
|
|
504
505
|
if (listener != null) {
|
|
505
|
-
listener.onCameraStartError("Camera start aborted: stop requested");
|
|
506
|
+
listener.onCameraStartError(this, "Camera start aborted: stop requested");
|
|
506
507
|
}
|
|
507
508
|
return;
|
|
508
509
|
}
|
|
509
510
|
}
|
|
510
511
|
if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
511
512
|
if (listener != null) {
|
|
512
|
-
listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
|
|
513
|
+
listener.onCameraStartError(this, "Camera start aborted: lifecycle destroyed");
|
|
513
514
|
}
|
|
514
515
|
return;
|
|
515
516
|
}
|
|
516
517
|
if (stopRequested) {
|
|
517
518
|
if (listener != null) {
|
|
518
|
-
listener.onCameraStartError("Camera start aborted: stop requested");
|
|
519
|
+
listener.onCameraStartError(this, "Camera start aborted: stop requested");
|
|
519
520
|
}
|
|
520
521
|
return;
|
|
521
522
|
}
|
|
522
523
|
lifecycleRegistry.setCurrentState(Lifecycle.State.STARTED);
|
|
523
524
|
if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
524
525
|
if (listener != null) {
|
|
525
|
-
listener.onCameraStartError("Camera start aborted: lifecycle destroyed");
|
|
526
|
+
listener.onCameraStartError(this, "Camera start aborted: lifecycle destroyed");
|
|
526
527
|
}
|
|
527
528
|
return;
|
|
528
529
|
}
|
|
529
530
|
if (stopRequested) {
|
|
530
531
|
if (listener != null) {
|
|
531
|
-
listener.onCameraStartError("Camera start aborted: stop requested");
|
|
532
|
+
listener.onCameraStartError(this, "Camera start aborted: stop requested");
|
|
532
533
|
}
|
|
533
534
|
return;
|
|
534
535
|
}
|
|
535
536
|
|
|
536
537
|
this.sessionConfig = config;
|
|
538
|
+
cameraStartedCallbackSent = false;
|
|
537
539
|
cameraExecutor = Executors.newSingleThreadExecutor();
|
|
538
540
|
requestEnumeratedDeviceCacheRefresh();
|
|
539
541
|
|
|
@@ -629,6 +631,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
629
631
|
if (cameraProvider != null) {
|
|
630
632
|
cameraProvider.unbindAll();
|
|
631
633
|
}
|
|
634
|
+
barcodeAnalysis = null;
|
|
632
635
|
if (cameraExecutor != null) {
|
|
633
636
|
cameraExecutor.shutdown();
|
|
634
637
|
}
|
|
@@ -654,7 +657,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
654
657
|
private void restoreWebViewBackground() {
|
|
655
658
|
// Capture sessionConfig reference once to avoid race conditions
|
|
656
659
|
CameraSessionConfiguration config = sessionConfig;
|
|
657
|
-
boolean shouldRestore = config
|
|
660
|
+
boolean shouldRestore = config == null || !config.isToBack();
|
|
658
661
|
if (shouldRestore) {
|
|
659
662
|
// Capture background color before posting to UI thread
|
|
660
663
|
final int backgroundColorToRestore = originalWebViewBackground;
|
|
@@ -674,26 +677,26 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
674
677
|
try {
|
|
675
678
|
if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
676
679
|
if (listener != null) {
|
|
677
|
-
listener.onCameraStartError("Camera binding cancelled: lifecycle destroyed (before provider)");
|
|
680
|
+
listener.onCameraStartError(this, "Camera binding cancelled: lifecycle destroyed (before provider)");
|
|
678
681
|
}
|
|
679
682
|
return;
|
|
680
683
|
}
|
|
681
684
|
if (stopRequested) {
|
|
682
685
|
if (listener != null) {
|
|
683
|
-
listener.onCameraStartError("Camera binding cancelled: stop requested (before provider)");
|
|
686
|
+
listener.onCameraStartError(this, "Camera binding cancelled: stop requested (before provider)");
|
|
684
687
|
}
|
|
685
688
|
return;
|
|
686
689
|
}
|
|
687
690
|
cameraProvider = cameraProviderFuture.get();
|
|
688
691
|
if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
689
692
|
if (listener != null) {
|
|
690
|
-
listener.onCameraStartError("Camera binding cancelled: lifecycle destroyed (after provider)");
|
|
693
|
+
listener.onCameraStartError(this, "Camera binding cancelled: lifecycle destroyed (after provider)");
|
|
691
694
|
}
|
|
692
695
|
return;
|
|
693
696
|
}
|
|
694
697
|
if (stopRequested) {
|
|
695
698
|
if (listener != null) {
|
|
696
|
-
listener.onCameraStartError("Camera binding cancelled: stop requested (after provider)");
|
|
699
|
+
listener.onCameraStartError(this, "Camera binding cancelled: stop requested (after provider)");
|
|
697
700
|
}
|
|
698
701
|
return;
|
|
699
702
|
}
|
|
@@ -703,7 +706,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
703
706
|
// Restore webView background on error
|
|
704
707
|
restoreWebViewBackground();
|
|
705
708
|
if (listener != null) {
|
|
706
|
-
listener.onCameraStartError("Error initializing camera: " + e.getMessage());
|
|
709
|
+
listener.onCameraStartError(this, "Error initializing camera: " + e.getMessage());
|
|
707
710
|
}
|
|
708
711
|
}
|
|
709
712
|
},
|
|
@@ -715,13 +718,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
715
718
|
if (previewView != null) {
|
|
716
719
|
removePreviewView();
|
|
717
720
|
}
|
|
718
|
-
if (sessionConfig.isToBack()) {
|
|
719
|
-
// Set to black initially to prevent flickering, will be transparent after camera starts
|
|
720
|
-
webView.setBackgroundColor(android.graphics.Color.BLACK);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
721
|
// Create a container to hold both the preview and grid overlay
|
|
724
722
|
previewContainer = new FrameLayout(context);
|
|
723
|
+
if (sessionConfig != null && sessionConfig.isToBack()) {
|
|
724
|
+
previewContainer.setBackgroundColor(Color.TRANSPARENT);
|
|
725
|
+
}
|
|
725
726
|
// Ensure container can receive touch events
|
|
726
727
|
previewContainer.setClickable(true);
|
|
727
728
|
previewContainer.setFocusable(true);
|
|
@@ -735,9 +736,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
735
736
|
|
|
736
737
|
// Create and setup the preview view
|
|
737
738
|
previewView = new PreviewView(context);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
739
|
+
if (sessionConfig != null && sessionConfig.isToBack()) {
|
|
740
|
+
previewView.setBackgroundColor(Color.TRANSPARENT);
|
|
741
|
+
}
|
|
742
|
+
PreviewView.ImplementationMode implementationMode = choosePreviewImplementationMode();
|
|
743
|
+
previewView.setImplementationMode(implementationMode);
|
|
741
744
|
// Set scale type based on aspectMode: 'contain' uses FIT, 'cover' uses FILL
|
|
742
745
|
String aspectMode = sessionConfig != null ? sessionConfig.getAspectMode() : "contain";
|
|
743
746
|
previewView.setScaleType("cover".equals(aspectMode) ? PreviewView.ScaleType.FILL_CENTER : PreviewView.ScaleType.FIT_CENTER);
|
|
@@ -753,6 +756,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
753
756
|
new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
|
754
757
|
);
|
|
755
758
|
|
|
759
|
+
previewView
|
|
760
|
+
.getPreviewStreamState()
|
|
761
|
+
.observe(this, (streamState) -> {
|
|
762
|
+
if (sessionConfig != null && sessionConfig.isToBack() && streamState == PreviewView.StreamState.STREAMING) {
|
|
763
|
+
notifyCameraStartedIfNeeded("streaming");
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
|
|
756
767
|
// Create and setup the grid overlay
|
|
757
768
|
gridOverlayView = new GridOverlayView(context);
|
|
758
769
|
// Make grid overlay not intercept touch events
|
|
@@ -781,7 +792,17 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
781
792
|
if (parent != null) {
|
|
782
793
|
FrameLayout.LayoutParams layoutParams = calculatePreviewLayoutParams();
|
|
783
794
|
parent.addView(previewContainer, layoutParams);
|
|
784
|
-
if (sessionConfig.isToBack())
|
|
795
|
+
if (sessionConfig.isToBack()) {
|
|
796
|
+
webView.bringToFront();
|
|
797
|
+
parent.requestTransparentRegion(webView);
|
|
798
|
+
webView.post(() -> {
|
|
799
|
+
webView.bringToFront();
|
|
800
|
+
ViewGroup currentParent = (ViewGroup) webView.getParent();
|
|
801
|
+
if (currentParent != null) {
|
|
802
|
+
currentParent.requestTransparentRegion(webView);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}
|
|
785
806
|
|
|
786
807
|
// Log the actual position after layout
|
|
787
808
|
previewContainer.post(() -> {
|
|
@@ -811,6 +832,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
811
832
|
}
|
|
812
833
|
}
|
|
813
834
|
|
|
835
|
+
private PreviewView.ImplementationMode choosePreviewImplementationMode() {
|
|
836
|
+
return PreviewView.ImplementationMode.COMPATIBLE;
|
|
837
|
+
}
|
|
838
|
+
|
|
814
839
|
/**
|
|
815
840
|
* Compute layout parameters for the camera preview container based on the current session configuration,
|
|
816
841
|
* device screen size, WebView/parent geometry, and optional aspect-ratio centering.
|
|
@@ -1000,7 +1025,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1000
1025
|
if (focusIndicatorView != null) {
|
|
1001
1026
|
focusIndicatorView = null;
|
|
1002
1027
|
}
|
|
1003
|
-
|
|
1028
|
+
if (sessionConfig == null || !sessionConfig.isToBack()) {
|
|
1029
|
+
webView.setBackgroundColor(originalWebViewBackground);
|
|
1030
|
+
}
|
|
1004
1031
|
}
|
|
1005
1032
|
|
|
1006
1033
|
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
@@ -1050,6 +1077,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1050
1077
|
.setTargetRotation(rotation)
|
|
1051
1078
|
.build();
|
|
1052
1079
|
sampleImageCapture = imageCapture;
|
|
1080
|
+
barcodeAnalysis = null;
|
|
1081
|
+
|
|
1082
|
+
if (!sessionConfig.isVideoModeEnabled() && sessionConfig.isBarcodeScannerEnabled()) {
|
|
1083
|
+
barcodeAnalysis = createBarcodeAnalysisUseCase();
|
|
1084
|
+
if (isBarcodeScannerActive && barcodeScanner != null && cameraExecutor != null) {
|
|
1085
|
+
barcodeAnalysis.setAnalyzer(cameraExecutor, this::analyzeBarcodeImage);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1053
1088
|
|
|
1054
1089
|
// Only setup VideoCapture if enableVideoMode is true
|
|
1055
1090
|
if (sessionConfig.isVideoModeEnabled()) {
|
|
@@ -1214,6 +1249,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1214
1249
|
if (initialZoom < minZoom || initialZoom > maxZoom) {
|
|
1215
1250
|
if (listener != null) {
|
|
1216
1251
|
listener.onCameraStartError(
|
|
1252
|
+
this,
|
|
1217
1253
|
"Initial zoom level " +
|
|
1218
1254
|
initialZoom +
|
|
1219
1255
|
" is not available. " +
|
|
@@ -1233,43 +1269,78 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1233
1269
|
isRunning = true;
|
|
1234
1270
|
Log.d(TAG, "bindCameraUseCases: Camera bound successfully");
|
|
1235
1271
|
if (listener != null) {
|
|
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
|
-
|
|
1272
|
+
if (sessionConfig != null && sessionConfig.isToBack()) {
|
|
1273
|
+
PreviewView.StreamState streamState = previewView != null ? previewView.getPreviewStreamState().getValue() : null;
|
|
1274
|
+
if (streamState == PreviewView.StreamState.STREAMING) {
|
|
1275
|
+
notifyCameraStartedIfNeeded("already-streaming");
|
|
1276
|
+
} else if (previewContainer != null) {
|
|
1277
|
+
previewContainer.postDelayed(
|
|
1278
|
+
() -> {
|
|
1279
|
+
PreviewView.StreamState latestState = previewView != null
|
|
1280
|
+
? previewView.getPreviewStreamState().getValue()
|
|
1281
|
+
: null;
|
|
1282
|
+
if (latestState == PreviewView.StreamState.STREAMING) {
|
|
1283
|
+
notifyCameraStartedIfNeeded("watchdog-streaming");
|
|
1284
|
+
}
|
|
1285
|
+
},
|
|
1286
|
+
300
|
|
1287
|
+
);
|
|
1288
|
+
previewContainer.postDelayed(
|
|
1289
|
+
() -> {
|
|
1290
|
+
PreviewView.StreamState latestState = previewView != null
|
|
1291
|
+
? previewView.getPreviewStreamState().getValue()
|
|
1292
|
+
: null;
|
|
1293
|
+
if (!cameraStartedCallbackSent && latestState == PreviewView.StreamState.STREAMING) {
|
|
1294
|
+
notifyCameraStartedIfNeeded("fallback-streaming");
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
1500
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
} else {
|
|
1301
|
+
notifyCameraStartedIfNeeded("bound");
|
|
1302
|
+
}
|
|
1264
1303
|
}
|
|
1265
1304
|
} catch (Exception e) {
|
|
1266
1305
|
// Restore webView background on error
|
|
1267
1306
|
restoreWebViewBackground();
|
|
1268
|
-
if (listener != null) listener.onCameraStartError("Error binding camera: " + e.getMessage());
|
|
1307
|
+
if (listener != null) listener.onCameraStartError(this, "Error binding camera: " + e.getMessage());
|
|
1269
1308
|
}
|
|
1270
1309
|
});
|
|
1271
1310
|
}
|
|
1272
1311
|
|
|
1312
|
+
private void notifyCameraStartedIfNeeded(String reason) {
|
|
1313
|
+
if (cameraStartedCallbackSent || listener == null || previewContainer == null) {
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
cameraStartedCallbackSent = true;
|
|
1317
|
+
previewContainer.post(() -> {
|
|
1318
|
+
if (listener == null || previewContainer == null) {
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
int actualWidth = getPreviewWidth();
|
|
1323
|
+
int actualHeight = getPreviewHeight();
|
|
1324
|
+
int actualX = getPreviewX();
|
|
1325
|
+
int actualY = getPreviewY();
|
|
1326
|
+
|
|
1327
|
+
Log.d(
|
|
1328
|
+
TAG,
|
|
1329
|
+
"onCameraStarted callback - actualX=" +
|
|
1330
|
+
actualX +
|
|
1331
|
+
", actualY=" +
|
|
1332
|
+
actualY +
|
|
1333
|
+
", actualWidth=" +
|
|
1334
|
+
actualWidth +
|
|
1335
|
+
", actualHeight=" +
|
|
1336
|
+
actualHeight
|
|
1337
|
+
);
|
|
1338
|
+
|
|
1339
|
+
updateGridOverlayBounds();
|
|
1340
|
+
listener.onCameraStarted(actualWidth, actualHeight, actualX, actualY);
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1273
1344
|
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
1274
1345
|
private CameraSelector buildCameraSelector() {
|
|
1275
1346
|
return buildCameraBindingPlan(sessionConfig).selector;
|
|
@@ -1523,6 +1594,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1523
1594
|
private void bindConfiguredUseCases(CameraBindingPlan bindingPlan, Preview preview) {
|
|
1524
1595
|
if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
|
|
1525
1596
|
camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, videoCapture);
|
|
1597
|
+
} else if (barcodeAnalysis != null) {
|
|
1598
|
+
camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, barcodeAnalysis);
|
|
1526
1599
|
} else {
|
|
1527
1600
|
camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture);
|
|
1528
1601
|
}
|
|
@@ -1551,26 +1624,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1551
1624
|
|
|
1552
1625
|
mainExecutor.execute(() -> {
|
|
1553
1626
|
try {
|
|
1554
|
-
stopBarcodeScannerInternal(
|
|
1627
|
+
stopBarcodeScannerInternal(false);
|
|
1555
1628
|
barcodeScanner = createBarcodeScanner(formats);
|
|
1556
1629
|
barcodeDetectionIntervalMs = Math.max(100L, detectionIntervalMs);
|
|
1557
1630
|
lastBarcodeFrameAtMs = 0L;
|
|
1558
1631
|
isBarcodeFrameProcessing = false;
|
|
1559
1632
|
isBarcodeScannerActive = true;
|
|
1560
1633
|
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
)
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
1569
|
-
.setResolutionSelector(barcodeResolutionSelector)
|
|
1570
|
-
.build();
|
|
1571
|
-
barcodeAnalysis.setAnalyzer(cameraExecutor, this::analyzeBarcodeImage);
|
|
1572
|
-
|
|
1573
|
-
cameraProvider.bindToLifecycle(this, currentCameraSelector, barcodeAnalysis);
|
|
1634
|
+
if (barcodeAnalysis == null) {
|
|
1635
|
+
barcodeAnalysis = createBarcodeAnalysisUseCase();
|
|
1636
|
+
barcodeAnalysis.setAnalyzer(cameraExecutor, this::analyzeBarcodeImage);
|
|
1637
|
+
cameraProvider.bindToLifecycle(this, currentCameraSelector, barcodeAnalysis);
|
|
1638
|
+
} else {
|
|
1639
|
+
barcodeAnalysis.setAnalyzer(cameraExecutor, this::analyzeBarcodeImage);
|
|
1640
|
+
}
|
|
1574
1641
|
callback.onStarted();
|
|
1575
1642
|
} catch (Exception e) {
|
|
1576
1643
|
stopBarcodeScannerInternal(true);
|
|
@@ -1583,6 +1650,17 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1583
1650
|
mainExecutor.execute(() -> stopBarcodeScannerInternal(true));
|
|
1584
1651
|
}
|
|
1585
1652
|
|
|
1653
|
+
private ImageAnalysis createBarcodeAnalysisUseCase() {
|
|
1654
|
+
ResolutionSelector barcodeResolutionSelector = new ResolutionSelector.Builder()
|
|
1655
|
+
.setResolutionStrategy(new ResolutionStrategy(new Size(1280, 720), ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER))
|
|
1656
|
+
.build();
|
|
1657
|
+
|
|
1658
|
+
return new ImageAnalysis.Builder()
|
|
1659
|
+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
1660
|
+
.setResolutionSelector(barcodeResolutionSelector)
|
|
1661
|
+
.build();
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1586
1664
|
private void stopBarcodeScannerInternal(boolean unbindAnalysis) {
|
|
1587
1665
|
isBarcodeScannerActive = false;
|
|
1588
1666
|
isBarcodeFrameProcessing = false;
|
|
@@ -1595,9 +1673,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1595
1673
|
cameraProvider.unbind(barcodeAnalysis);
|
|
1596
1674
|
} catch (Exception e) {
|
|
1597
1675
|
Log.w(TAG, "stopBarcodeScannerInternal: failed to unbind barcode analysis", e);
|
|
1676
|
+
} finally {
|
|
1677
|
+
barcodeAnalysis = null;
|
|
1598
1678
|
}
|
|
1599
1679
|
}
|
|
1600
|
-
barcodeAnalysis = null;
|
|
1601
1680
|
}
|
|
1602
1681
|
|
|
1603
1682
|
if (barcodeScanner != null) {
|
|
@@ -1803,6 +1882,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1803
1882
|
target.setCentered(source.isCentered());
|
|
1804
1883
|
target.setTargetZoom(source.getTargetZoom());
|
|
1805
1884
|
target.setEnablePhysicalDeviceSelection(source.isPhysicalDeviceSelectionEnabled());
|
|
1885
|
+
target.setBarcodeScannerEnabled(source.isBarcodeScannerEnabled());
|
|
1806
1886
|
}
|
|
1807
1887
|
|
|
1808
1888
|
private void requestEnumeratedDeviceCacheRefresh() {
|
|
@@ -27,6 +27,7 @@ public class CameraSessionConfiguration {
|
|
|
27
27
|
private boolean isCentered = false;
|
|
28
28
|
private final String videoQuality;
|
|
29
29
|
private boolean enablePhysicalDeviceSelection = false;
|
|
30
|
+
private boolean barcodeScannerEnabled = false;
|
|
30
31
|
|
|
31
32
|
public CameraSessionConfiguration(
|
|
32
33
|
String deviceId,
|
|
@@ -158,6 +159,14 @@ public class CameraSessionConfiguration {
|
|
|
158
159
|
this.enablePhysicalDeviceSelection = enablePhysicalDeviceSelection;
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
public boolean isBarcodeScannerEnabled() {
|
|
163
|
+
return barcodeScannerEnabled;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public void setBarcodeScannerEnabled(boolean barcodeScannerEnabled) {
|
|
167
|
+
this.barcodeScannerEnabled = barcodeScannerEnabled;
|
|
168
|
+
}
|
|
169
|
+
|
|
161
170
|
// Additional getters with "get" prefix for compatibility
|
|
162
171
|
public boolean getToBack() {
|
|
163
172
|
return toBack;
|
|
@@ -35,7 +35,7 @@ extension UIWindow {
|
|
|
35
35
|
*/
|
|
36
36
|
@objc(CameraPreview)
|
|
37
37
|
public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
|
|
38
|
-
private let pluginVersion: String = "8.4.
|
|
38
|
+
private let pluginVersion: String = "8.4.4"
|
|
39
39
|
public let identifier = "CameraPreviewPlugin"
|
|
40
40
|
public let jsName = "CameraPreview"
|
|
41
41
|
public let pluginMethods: [CAPPluginMethod] = [
|