@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.
@@ -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
- // Restore original window background if modified earlier
528
- if (originalWindowBackground != null) {
529
- try {
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
- try {
1057
- if (originalWindowBackground == null) {
1058
- originalWindowBackground = getBridge().getActivity().getWindow().getDecorView().getBackground();
1059
- }
1060
- // Set to solid black first to prevent flickering during transition
1061
- // This provides a stable base before camera preview is ready
1062
- getBridge().getActivity().getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
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
- restoreWebViewVisualState();
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 applyTransparentBackgroundsForToBack() {
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
- if (originalWebViewAlpha == null) {
1755
- originalWebViewAlpha = webView.getAlpha();
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
- activity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
1766
- if (webViewParent != null) {
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
- // Keep a tiny alpha on MIUI/Xiaomi devices to avoid compositor bugs that treat
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 = originalWebViewAlpha;
1791
- final Drawable parentBackground = originalWebViewParentBackground;
1792
- originalWebViewAlpha = null;
1793
- originalWebViewParentBackground = null;
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 (webViewParent != null && parentBackground != null) {
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
- // Restore original window background on error to prevent black screen
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 != null && config.isToBack();
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
- // Use TextureView-backed implementation for broader device compatibility when overlaying with WebView
739
- // This avoids SurfaceView z-order issues seen on some MIUI/EMUI devices.
740
- previewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
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()) webView.bringToFront();
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
- webView.setBackgroundColor(originalWebViewBackground);
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
- // Post the callback to ensure layout is complete
1237
- previewContainer.post(() -> {
1238
- // Return actual preview container dimensions instead of requested dimensions
1239
- // Get the actual camera dimensions and position
1240
- int actualWidth = getPreviewWidth();
1241
- int actualHeight = getPreviewHeight();
1242
- int actualX = getPreviewX();
1243
- int actualY = getPreviewY();
1244
-
1245
- Log.d(
1246
- TAG,
1247
- "onCameraStarted callback - actualX=" +
1248
- actualX +
1249
- ", actualY=" +
1250
- actualY +
1251
- ", actualWidth=" +
1252
- actualWidth +
1253
- ", actualHeight=" +
1254
- actualHeight
1255
- );
1256
-
1257
- // Update grid overlay bounds after camera is started
1258
- updateGridOverlayBounds();
1259
-
1260
- // Notify listener that camera is started
1261
- // The listener (CameraPreview) will handle setting both window and webview to transparent
1262
- listener.onCameraStarted(actualWidth, actualHeight, actualX, actualY);
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(true);
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
- ResolutionSelector barcodeResolutionSelector = new ResolutionSelector.Builder()
1562
- .setResolutionStrategy(
1563
- new ResolutionStrategy(new Size(1280, 720), ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER)
1564
- )
1565
- .build();
1566
-
1567
- barcodeAnalysis = new ImageAnalysis.Builder()
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.2"
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] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.4.2",
3
+ "version": "8.4.4",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {