@capgo/camera-preview 8.11.1 → 8.11.3

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.
@@ -1,6 +1,6 @@
1
1
  distributionBase=GRADLE_USER_HOME
2
2
  distributionPath=wrapper/dists
3
- distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-all.zip
3
+ distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-all.zip
4
4
  networkTimeout=10000
5
5
  validateDistributionUrl=true
6
6
  zipStoreBase=GRADLE_USER_HOME
@@ -15,7 +15,6 @@ import android.content.pm.ActivityInfo;
15
15
  import android.content.pm.PackageManager;
16
16
  import android.content.res.Configuration;
17
17
  import android.graphics.Color;
18
- import android.graphics.drawable.ColorDrawable;
19
18
  import android.graphics.drawable.Drawable;
20
19
  import android.location.Location;
21
20
  import android.net.Uri;
@@ -54,7 +53,6 @@ import java.util.ArrayList;
54
53
  import java.util.Arrays;
55
54
  import java.util.Collections;
56
55
  import java.util.List;
57
- import java.util.Locale;
58
56
  import java.util.Objects;
59
57
  import org.json.JSONArray;
60
58
  import org.json.JSONObject;
@@ -104,11 +102,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
104
102
  cameraXView.setListener(this);
105
103
  }
106
104
  if (lastSessionConfig.isToBack()) {
107
- if (usesFullStackTransparentBackgroundWorkaround()) {
108
- activateTransparentBackgroundsForToBack(cameraXView);
109
- } else {
110
- prepareTransparentBackgroundsForToBack(cameraXView);
111
- }
105
+ applyToBackVisualState(cameraXView);
112
106
  } else {
113
107
  toBackVisualStateActive = false;
114
108
  }
@@ -128,9 +122,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
128
122
  lastSessionConfig = null;
129
123
  clearActiveBarcodeScanner();
130
124
  toBackVisualStateActive = false;
131
- restoreOriginalWindowBackground(getBridge().getActivity());
132
125
  restoreWebViewVisualState();
133
- restoreSystemUiForToBackMode(getBridge().getActivity());
134
126
  unregisterScreenLockReceiver();
135
127
  }
136
128
 
@@ -144,9 +136,10 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
144
136
  static final String CAMERA_WITH_LOCATION_PERMISSION_ALIAS = "cameraWithLocation";
145
137
  static final String MICROPHONE_ONLY_PERMISSION_ALIAS = "microphoneOnly";
146
138
 
147
- private String captureCallbackId = "";
148
- private String sampleCallbackId = "";
149
- private String cameraStartCallbackId = "";
139
+ private PluginCall pendingCaptureCall;
140
+ private PluginCall pendingSampleCall;
141
+ private PluginCall pendingCameraStartedCall;
142
+ private PluginCall pendingStopRecordCall;
150
143
  private final Object pendingStartLock = new Object();
151
144
  private PluginCall pendingStartCall;
152
145
  private int previousOrientationRequest = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
@@ -159,16 +152,12 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
159
152
  private String lastOrientationStr = "unknown";
160
153
  private boolean lastDisableAudio = true;
161
154
  private boolean lastIncludeSafeAreaInsets = false;
162
- private Drawable originalWindowBackground;
163
- private boolean originalWindowBackgroundCaptured = false;
164
155
  private Drawable originalWebViewBackground;
165
156
  private boolean originalWebViewBackgroundCaptured = false;
166
157
  private Float originalWebViewAlpha;
158
+ private Integer originalWebViewLayerType;
167
159
  private Drawable originalWebViewParentBackground;
168
160
  private boolean originalWebViewParentBackgroundCaptured = false;
169
- private Integer originalStatusBarColor;
170
- private Integer originalNavigationBarColor;
171
- private Boolean originalNavigationBarContrastEnforced;
172
161
  private volatile boolean toBackVisualStateActive = false;
173
162
  private boolean isCameraPermissionDialogShowing = false;
174
163
  private boolean pendingStartBarcodeScanner = false;
@@ -518,8 +507,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
518
507
  }
519
508
 
520
509
  private void proceedWithCapture(PluginCall call, Location location) {
521
- bridge.saveCall(call);
522
- captureCallbackId = call.getCallbackId();
510
+ call.setKeepAlive(true);
511
+ pendingCaptureCall = call;
523
512
 
524
513
  Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
525
514
  final boolean saveToGallery = Boolean.TRUE.equals(call.getBoolean("saveToGallery"));
@@ -538,8 +527,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
538
527
  call.reject("Camera is not running");
539
528
  return;
540
529
  }
541
- bridge.saveCall(call);
542
- sampleCallbackId = call.getCallbackId();
530
+ call.setKeepAlive(true);
531
+ pendingSampleCall = call;
543
532
  Integer quality = Objects.requireNonNull(call.getInt("quality", 85));
544
533
  final boolean mirrorFrontCamera = Boolean.TRUE.equals(call.getBoolean("mirrorFrontCamera"));
545
534
  cameraXView.captureSample(quality, mirrorFrontCamera);
@@ -798,6 +787,9 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
798
787
  ) {
799
788
  cameraXView = new CameraXView(getContext(), getBridge().getWebView());
800
789
  cameraXView.setListener(this);
790
+ if (lastSessionConfig.isToBack()) {
791
+ applyToBackVisualState(cameraXView);
792
+ }
801
793
  requestBarcodeScannerRestartAfterCameraResume();
802
794
  cameraRestartAfterResumeInProgress = true;
803
795
  cameraXView.startSession(lastSessionConfig);
@@ -840,9 +832,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
840
832
  lastSessionConfig = null;
841
833
  clearActiveBarcodeScanner();
842
834
  toBackVisualStateActive = false;
843
- restoreOriginalWindowBackground(getBridge().getActivity());
844
835
  restoreWebViewVisualState();
845
- restoreSystemUiForToBackMode(getBridge().getActivity());
846
836
  call.resolve();
847
837
  });
848
838
  }
@@ -1360,13 +1350,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1360
1350
  getBridge()
1361
1351
  .getActivity()
1362
1352
  .runOnUiThread(() -> {
1363
- lockSystemUiForToBackMode(getBridge().getActivity(), toBack);
1364
1353
  if (toBack) {
1365
- if (usesFullStackTransparentBackgroundWorkaround()) {
1366
- activateTransparentBackgroundsForToBack(cameraXView);
1367
- } else {
1368
- prepareTransparentBackgroundsForToBack(cameraXView);
1369
- }
1354
+ applyToBackVisualState(cameraXView);
1370
1355
  } else {
1371
1356
  toBackVisualStateActive = false;
1372
1357
  }
@@ -1651,8 +1636,8 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1651
1636
  clearActiveBarcodeScanner();
1652
1637
  }
1653
1638
 
1654
- bridge.saveCall(call);
1655
- cameraStartCallbackId = call.getCallbackId();
1639
+ call.setKeepAlive(true);
1640
+ pendingCameraStartedCall = call;
1656
1641
  cameraXView.startSession(config);
1657
1642
 
1658
1643
  // Setup orientation listener to mirror iOS screenResize emission
@@ -1908,7 +1893,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1908
1893
 
1909
1894
  @Override
1910
1895
  public void onPictureTaken(String base64, JSONObject exif) {
1911
- PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
1896
+ PluginCall pluginCall = pendingCaptureCall;
1912
1897
  if (pluginCall == null) {
1913
1898
  Log.e("CameraPreview", "onPictureTaken: captureCallbackId is null");
1914
1899
  return;
@@ -1917,18 +1902,20 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1917
1902
  result.put("value", base64);
1918
1903
  result.put("exif", exif);
1919
1904
  pluginCall.resolve(result);
1920
- bridge.releaseCall(pluginCall);
1905
+ pluginCall.setKeepAlive(false);
1906
+ pendingCaptureCall = null;
1921
1907
  }
1922
1908
 
1923
1909
  @Override
1924
1910
  public void onPictureTakenError(String message) {
1925
- PluginCall pluginCall = bridge.getSavedCall(captureCallbackId);
1911
+ PluginCall pluginCall = pendingCaptureCall;
1926
1912
  if (pluginCall == null) {
1927
1913
  Log.e("CameraPreview", "onPictureTakenError: captureCallbackId is null");
1928
1914
  return;
1929
1915
  }
1930
1916
  pluginCall.reject(message);
1931
- bridge.releaseCall(pluginCall);
1917
+ pluginCall.setKeepAlive(false);
1918
+ pendingCaptureCall = null;
1932
1919
  }
1933
1920
 
1934
1921
  @Override
@@ -1985,36 +1972,6 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
1985
1972
  return false;
1986
1973
  }
1987
1974
 
1988
- private boolean isMiuiDevice() {
1989
- String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER.toLowerCase(Locale.US) : "";
1990
- String brand = Build.BRAND != null ? Build.BRAND.toLowerCase(Locale.US) : "";
1991
- return manufacturer.contains("xiaomi") || brand.contains("xiaomi") || brand.contains("redmi") || brand.contains("poco");
1992
- }
1993
-
1994
- private boolean usesFullStackTransparentBackgroundWorkaround() {
1995
- String manufacturer = Build.MANUFACTURER != null ? Build.MANUFACTURER.toLowerCase(Locale.US) : "";
1996
- String brand = Build.BRAND != null ? Build.BRAND.toLowerCase(Locale.US) : "";
1997
- return (
1998
- isMiuiDevice() ||
1999
- manufacturer.contains("huawei") ||
2000
- manufacturer.contains("honor") ||
2001
- brand.contains("huawei") ||
2002
- brand.contains("honor")
2003
- );
2004
- }
2005
-
2006
- private void captureOriginalWindowBackground(Activity activity) {
2007
- if (activity == null) {
2008
- return;
2009
- }
2010
- synchronized (this) {
2011
- if (!originalWindowBackgroundCaptured) {
2012
- originalWindowBackground = activity.getWindow().getDecorView().getBackground();
2013
- originalWindowBackgroundCaptured = true;
2014
- }
2015
- }
2016
- }
2017
-
2018
1975
  private void captureOriginalWebViewVisualState(WebView webView, ViewGroup webViewParent) {
2019
1976
  if (webView == null) {
2020
1977
  return;
@@ -2027,6 +1984,9 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2027
1984
  if (originalWebViewAlpha == null) {
2028
1985
  originalWebViewAlpha = webView.getAlpha();
2029
1986
  }
1987
+ if (originalWebViewLayerType == null) {
1988
+ originalWebViewLayerType = webView.getLayerType();
1989
+ }
2030
1990
  if (webViewParent != null && !originalWebViewParentBackgroundCaptured) {
2031
1991
  originalWebViewParentBackground = webViewParent.getBackground();
2032
1992
  originalWebViewParentBackgroundCaptured = true;
@@ -2034,95 +1994,7 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2034
1994
  }
2035
1995
  }
2036
1996
 
2037
- private void restoreOriginalWindowBackground(Activity activity) {
2038
- final Drawable backgroundToRestore;
2039
- final boolean captured;
2040
- synchronized (this) {
2041
- backgroundToRestore = originalWindowBackground;
2042
- captured = originalWindowBackgroundCaptured;
2043
- originalWindowBackground = null;
2044
- originalWindowBackgroundCaptured = false;
2045
- }
2046
-
2047
- if (!captured || activity == null) {
2048
- return;
2049
- }
2050
-
2051
- activity.runOnUiThread(() -> {
2052
- try {
2053
- activity.getWindow().setBackgroundDrawable(backgroundToRestore);
2054
- } catch (Exception e) {
2055
- Log.w(TAG, "Failed to restore window background", e);
2056
- }
2057
- });
2058
- }
2059
-
2060
- private int toOpaqueColor(int color) {
2061
- return Color.argb(255, Color.red(color), Color.green(color), Color.blue(color));
2062
- }
2063
-
2064
- private void lockSystemUiForToBackMode(Activity activity, boolean toBack) {
2065
- if (activity == null) {
2066
- return;
2067
- }
2068
- if (!toBack) {
2069
- restoreSystemUiForToBackMode(activity);
2070
- return;
2071
- }
2072
-
2073
- try {
2074
- if (originalStatusBarColor == null) {
2075
- originalStatusBarColor = activity.getWindow().getStatusBarColor();
2076
- }
2077
- if (originalNavigationBarColor == null) {
2078
- originalNavigationBarColor = activity.getWindow().getNavigationBarColor();
2079
- }
2080
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && originalNavigationBarContrastEnforced == null) {
2081
- originalNavigationBarContrastEnforced = activity.getWindow().isNavigationBarContrastEnforced();
2082
- }
2083
-
2084
- int statusBarColor = toOpaqueColor(originalStatusBarColor != null ? originalStatusBarColor : Color.BLACK);
2085
- int navigationBarColor = toOpaqueColor(originalNavigationBarColor != null ? originalNavigationBarColor : Color.BLACK);
2086
-
2087
- activity.getWindow().setStatusBarColor(statusBarColor);
2088
- activity.getWindow().setNavigationBarColor(navigationBarColor);
2089
-
2090
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
2091
- activity.getWindow().setNavigationBarContrastEnforced(false);
2092
- }
2093
- } catch (Exception e) {
2094
- Log.w(TAG, "Failed to lock system UI colors for toBack mode", e);
2095
- }
2096
- }
2097
-
2098
- private void restoreSystemUiForToBackMode(Activity activity) {
2099
- final Integer statusBarColor = originalStatusBarColor;
2100
- final Integer navigationBarColor = originalNavigationBarColor;
2101
- final Boolean navigationBarContrastEnforced = originalNavigationBarContrastEnforced;
2102
- originalStatusBarColor = null;
2103
- originalNavigationBarColor = null;
2104
- originalNavigationBarContrastEnforced = null;
2105
-
2106
- if (activity == null) {
2107
- return;
2108
- }
2109
-
2110
- activity.runOnUiThread(() -> {
2111
- try {
2112
- if (statusBarColor != null) {
2113
- activity.getWindow().setStatusBarColor(statusBarColor);
2114
- }
2115
- if (navigationBarColor != null) {
2116
- activity.getWindow().setNavigationBarColor(navigationBarColor);
2117
- }
2118
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && navigationBarContrastEnforced != null) {
2119
- activity.getWindow().setNavigationBarContrastEnforced(navigationBarContrastEnforced);
2120
- }
2121
- } catch (Exception ignored) {}
2122
- });
2123
- }
2124
-
2125
- private void prepareTransparentBackgroundsForToBack(CameraXView visualStateOwner) {
1997
+ private void applyToBackVisualState(CameraXView visualStateOwner) {
2126
1998
  Activity activity = getActivity();
2127
1999
  WebView webView = getBridge().getWebView();
2128
2000
  if (activity == null || webView == null) {
@@ -2131,34 +2003,20 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2131
2003
 
2132
2004
  toBackVisualStateActive = true;
2133
2005
  final ViewGroup webViewParent = (ViewGroup) webView.getParent();
2134
- captureOriginalWindowBackground(activity);
2135
2006
  captureOriginalWebViewVisualState(webView, webViewParent);
2136
- }
2137
-
2138
- private void activateTransparentBackgroundsForToBack(CameraXView visualStateOwner) {
2139
- prepareTransparentBackgroundsForToBack(visualStateOwner);
2140
- Activity activity = getActivity();
2141
- WebView webView = getBridge().getWebView();
2142
- if (activity == null || webView == null) {
2143
- return;
2144
- }
2145
-
2146
- final ViewGroup webViewParent = (ViewGroup) webView.getParent();
2147
2007
 
2148
2008
  Runnable apply = () -> {
2149
2009
  try {
2150
2010
  if (!toBackVisualStateActive || visualStateOwner == null || cameraXView != visualStateOwner) {
2151
2011
  return;
2152
2012
  }
2153
- boolean fullStackWorkaround = usesFullStackTransparentBackgroundWorkaround();
2154
- if (fullStackWorkaround) {
2155
- activity.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
2013
+ if (ToBackCompositorHelper.shouldTransparentizeWebViewParent() && webViewParent != null) {
2014
+ webViewParent.setBackgroundColor(ToBackCompositorHelper.resolveWebViewBackgroundColor());
2156
2015
  }
2157
- if (webViewParent != null && fullStackWorkaround) {
2158
- webViewParent.setBackgroundColor(Color.TRANSPARENT);
2159
- }
2160
- webView.setBackgroundColor(isMiuiDevice() ? Color.argb(1, 255, 255, 255) : Color.TRANSPARENT);
2161
- webView.setAlpha(isMiuiDevice() ? 0.99f : (originalWebViewAlpha != null ? originalWebViewAlpha : 1f));
2016
+ webView.setBackgroundColor(ToBackCompositorHelper.resolveWebViewBackgroundColor());
2017
+ webView.setLayerType(ToBackCompositorHelper.resolveWebViewLayerType(), null);
2018
+ float alpha = ToBackCompositorHelper.resolveWebViewAlpha(originalWebViewAlpha != null ? originalWebViewAlpha : 1f);
2019
+ webView.setAlpha(alpha);
2162
2020
  if (webViewParent != null) {
2163
2021
  webViewParent.requestTransparentRegion(webView);
2164
2022
  }
@@ -2167,20 +2025,14 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2167
2025
  }
2168
2026
  };
2169
2027
 
2170
- activity.runOnUiThread(() -> {
2171
- apply.run();
2172
- if (isMiuiDevice()) {
2173
- webView.postDelayed(apply, 50);
2174
- webView.postDelayed(apply, 250);
2175
- }
2176
- });
2028
+ activity.runOnUiThread(apply);
2177
2029
  }
2178
2030
 
2179
2031
  private void applyTransparentBackgroundsForToBack() {
2180
2032
  if (!isToBackMode()) {
2181
2033
  return;
2182
2034
  }
2183
- activateTransparentBackgroundsForToBack(cameraXView);
2035
+ applyToBackVisualState(cameraXView);
2184
2036
  }
2185
2037
 
2186
2038
  private void restoreWebViewVisualState() {
@@ -2191,24 +2043,27 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2191
2043
  Activity activity = getActivity();
2192
2044
  WebView webView = getBridge().getWebView();
2193
2045
  final Float alphaToRestore;
2046
+ final Integer layerTypeToRestore;
2194
2047
  final Drawable webViewBackground;
2195
2048
  final boolean webViewBackgroundCaptured;
2196
2049
  final Drawable parentBackground;
2197
2050
  final boolean parentBackgroundCaptured;
2198
2051
  synchronized (this) {
2199
2052
  alphaToRestore = originalWebViewAlpha;
2053
+ layerTypeToRestore = originalWebViewLayerType;
2200
2054
  webViewBackground = originalWebViewBackground;
2201
2055
  webViewBackgroundCaptured = originalWebViewBackgroundCaptured;
2202
2056
  parentBackground = originalWebViewParentBackground;
2203
2057
  parentBackgroundCaptured = originalWebViewParentBackgroundCaptured;
2204
2058
  originalWebViewAlpha = null;
2059
+ originalWebViewLayerType = null;
2205
2060
  originalWebViewBackground = null;
2206
2061
  originalWebViewBackgroundCaptured = false;
2207
2062
  originalWebViewParentBackground = null;
2208
2063
  originalWebViewParentBackgroundCaptured = false;
2209
2064
  }
2210
2065
 
2211
- if (alphaToRestore == null && !webViewBackgroundCaptured && !parentBackgroundCaptured) {
2066
+ if (alphaToRestore == null && layerTypeToRestore == null && !webViewBackgroundCaptured && !parentBackgroundCaptured) {
2212
2067
  return;
2213
2068
  }
2214
2069
 
@@ -2227,6 +2082,9 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2227
2082
  } else {
2228
2083
  webView.setBackgroundColor(DEFAULT_WEB_VIEW_BACKGROUND);
2229
2084
  }
2085
+ if (layerTypeToRestore != null) {
2086
+ webView.setLayerType(layerTypeToRestore, null);
2087
+ }
2230
2088
  if (webViewParent != null && parentBackgroundCaptured) {
2231
2089
  webViewParent.setBackground(parentBackground);
2232
2090
  }
@@ -2237,17 +2095,10 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2237
2095
  @Override
2238
2096
  public void onCameraStarted(int width, int height, int x, int y) {
2239
2097
  cameraRestartAfterResumeInProgress = false;
2240
- // Always transition window and WebView backgrounds to transparent when the camera starts,
2241
- // regardless of whether there is a pending JS call. This is critical for the
2242
- // background/foreground resume cycle: on resume, handleOnResume() sets backgrounds to
2243
- // black (to prevent flicker) and then restarts the camera session, but
2244
- // cameraStartCallbackId is null at that point. Without this unconditional block the
2245
- // window and WebView stay black after every background/foreground transition.
2246
- // Both backgrounds are set together in the same UI thread operation to avoid race
2247
- // conditions and compositor layering issues.
2098
+ // Re-apply WebView transparency after the preview is bound (idempotent safety net for resume).
2248
2099
  applyTransparentBackgroundsForToBack();
2249
2100
 
2250
- PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
2101
+ PluginCall call = pendingCameraStartedCall;
2251
2102
  if (call != null) {
2252
2103
  // Convert pixel values back to logical units
2253
2104
  DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
@@ -2417,27 +2268,27 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2417
2268
 
2418
2269
  private void resolveCameraStartCall(PluginCall call, JSObject result) {
2419
2270
  call.resolve(result);
2420
- bridge.releaseCall(call);
2421
- cameraStartCallbackId = null; // Prevent re-use
2271
+ call.setKeepAlive(false);
2272
+ pendingCameraStartedCall = null; // Prevent re-use
2422
2273
  resetPendingStartBarcodeScanner();
2423
2274
  }
2424
2275
 
2425
2276
  private void rejectCameraStartCall(PluginCall call, String message) {
2426
2277
  call.reject(message);
2427
- bridge.releaseCall(call);
2428
- cameraStartCallbackId = null;
2278
+ call.setKeepAlive(false);
2279
+ pendingCameraStartedCall = null;
2429
2280
  resetPendingStartBarcodeScanner();
2430
2281
  }
2431
2282
 
2432
2283
  @Override
2433
2284
  public void onSampleTaken(String result) {
2434
- PluginCall call = bridge.getSavedCall(sampleCallbackId);
2285
+ PluginCall call = pendingSampleCall;
2435
2286
  if (call != null) {
2436
2287
  JSObject ret = new JSObject();
2437
2288
  ret.put("value", result);
2438
2289
  call.resolve(ret);
2439
- bridge.releaseCall(call);
2440
- sampleCallbackId = null;
2290
+ call.setKeepAlive(false);
2291
+ pendingSampleCall = null;
2441
2292
  } else {
2442
2293
  Log.w("CameraPreview", "onSampleTaken: no pending call to resolve");
2443
2294
  }
@@ -2445,11 +2296,11 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2445
2296
 
2446
2297
  @Override
2447
2298
  public void onSampleTakenError(String message) {
2448
- PluginCall call = bridge.getSavedCall(sampleCallbackId);
2299
+ PluginCall call = pendingSampleCall;
2449
2300
  if (call != null) {
2450
2301
  call.reject(message);
2451
- bridge.releaseCall(call);
2452
- sampleCallbackId = null;
2302
+ call.setKeepAlive(false);
2303
+ pendingSampleCall = null;
2453
2304
  } else {
2454
2305
  Log.e("CameraPreview", "Sample taken error (no pending call): " + message);
2455
2306
  }
@@ -2495,17 +2346,15 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2495
2346
  }
2496
2347
  }
2497
2348
 
2498
- PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
2349
+ PluginCall call = pendingCameraStartedCall;
2499
2350
  if (call != null) {
2500
2351
  call.reject(message);
2501
- bridge.releaseCall(call);
2502
- cameraStartCallbackId = null;
2352
+ call.setKeepAlive(false);
2353
+ pendingCameraStartedCall = null;
2503
2354
  resetPendingStartBarcodeScanner();
2504
2355
  }
2505
2356
 
2506
- restoreOriginalWindowBackground(getBridge().getActivity());
2507
2357
  restoreWebViewVisualState();
2508
- restoreSystemUiForToBackMode(getBridge().getActivity());
2509
2358
  }
2510
2359
 
2511
2360
  @PluginMethod
@@ -2882,28 +2731,30 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
2882
2731
  }
2883
2732
 
2884
2733
  try {
2885
- bridge.saveCall(call);
2886
- final String cbId = call.getCallbackId();
2734
+ call.setKeepAlive(true);
2735
+ pendingStopRecordCall = call;
2887
2736
  cameraXView.stopRecordVideo(
2888
2737
  new CameraXView.VideoRecordingCallback() {
2889
2738
  @Override
2890
2739
  public void onSuccess(String filePath, String reason) {
2891
- PluginCall saved = bridge.getSavedCall(cbId);
2740
+ PluginCall saved = pendingStopRecordCall;
2741
+ pendingStopRecordCall = null;
2892
2742
  if (saved != null) {
2893
2743
  JSObject result = new JSObject();
2894
2744
  result.put("videoFilePath", filePath);
2895
2745
  result.put("reason", reason);
2896
2746
  saved.resolve(result);
2897
- bridge.releaseCall(saved);
2747
+ saved.setKeepAlive(false);
2898
2748
  }
2899
2749
  }
2900
2750
 
2901
2751
  @Override
2902
2752
  public void onError(String message) {
2903
- PluginCall saved = bridge.getSavedCall(cbId);
2753
+ PluginCall saved = pendingStopRecordCall;
2754
+ pendingStopRecordCall = null;
2904
2755
  if (saved != null) {
2905
2756
  saved.reject("Failed to stop video recording: " + message);
2906
- bridge.releaseCall(saved);
2757
+ saved.setKeepAlive(false);
2907
2758
  }
2908
2759
  }
2909
2760
  }
@@ -739,7 +739,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
739
739
  previewContainer.setFocusable(true);
740
740
 
741
741
  // Disable any potential drawing artifacts that might cause 1px offset
742
- previewContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
742
+ boolean toBack = sessionConfig != null && sessionConfig.isToBack();
743
+ if (ToBackCompositorHelper.shouldUseHardwareLayerOnPreviewContainer(toBack)) {
744
+ previewContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
745
+ } else {
746
+ previewContainer.setLayerType(View.LAYER_TYPE_NONE, null);
747
+ }
743
748
 
744
749
  // Ensure no clip bounds that might cause visual offset
745
750
  previewContainer.setClipChildren(false);
@@ -0,0 +1,36 @@
1
+ package app.capgo.capacitor.camera.preview;
2
+
3
+ import android.graphics.Color;
4
+ import android.view.View;
5
+
6
+ /**
7
+ * Policy for Android {@code toBack} compositing.
8
+ * <p>
9
+ * Camera preview sits behind the Capacitor WebView. Only the WebView stack should be made
10
+ * transparent — never the Activity window drawable or system bar colors. Touching those causes
11
+ * status/navigation bar flicker (Pixel/Android 16) and fights edge-to-edge scrims.
12
+ */
13
+ public final class ToBackCompositorHelper {
14
+
15
+ private ToBackCompositorHelper() {}
16
+
17
+ public static boolean shouldTransparentizeWebViewParent() {
18
+ return true;
19
+ }
20
+
21
+ public static int resolveWebViewBackgroundColor() {
22
+ return Color.TRANSPARENT;
23
+ }
24
+
25
+ public static float resolveWebViewAlpha(float originalAlpha) {
26
+ return originalAlpha;
27
+ }
28
+
29
+ public static boolean shouldUseHardwareLayerOnPreviewContainer(boolean toBack) {
30
+ return !toBack;
31
+ }
32
+
33
+ public static int resolveWebViewLayerType() {
34
+ return View.LAYER_TYPE_HARDWARE;
35
+ }
36
+ }
@@ -0,0 +1,39 @@
1
+ package app.capgo.capacitor.camera.preview;
2
+
3
+ import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertFalse;
5
+ import static org.junit.Assert.assertTrue;
6
+
7
+ import android.graphics.Color;
8
+ import android.view.View;
9
+ import org.junit.Test;
10
+
11
+ public class ToBackCompositorHelperTest {
12
+
13
+ @Test
14
+ public void transparentizesWebViewParentOnAllDevices() {
15
+ assertTrue(ToBackCompositorHelper.shouldTransparentizeWebViewParent());
16
+ }
17
+
18
+ @Test
19
+ public void webViewBackgroundIsFullyTransparent() {
20
+ assertEquals(Color.TRANSPARENT, ToBackCompositorHelper.resolveWebViewBackgroundColor());
21
+ }
22
+
23
+ @Test
24
+ public void preservesOriginalWebViewAlpha() {
25
+ assertEquals(0.75f, ToBackCompositorHelper.resolveWebViewAlpha(0.75f), 0.0001f);
26
+ assertEquals(1f, ToBackCompositorHelper.resolveWebViewAlpha(1f), 0.0001f);
27
+ }
28
+
29
+ @Test
30
+ public void skipsHardwareLayerOnPreviewContainerWhenToBack() {
31
+ assertFalse(ToBackCompositorHelper.shouldUseHardwareLayerOnPreviewContainer(true));
32
+ assertTrue(ToBackCompositorHelper.shouldUseHardwareLayerOnPreviewContainer(false));
33
+ }
34
+
35
+ @Test
36
+ public void usesHardwareLayerForWebViewTransparency() {
37
+ assertEquals(View.LAYER_TYPE_HARDWARE, ToBackCompositorHelper.resolveWebViewLayerType());
38
+ }
39
+ }
@@ -82,8 +82,8 @@ class CameraController: NSObject {
82
82
  }
83
83
  }
84
84
 
85
- // 2. Reset Exposure to the center ONLY if it is not explicitly locked
86
- if device.exposureMode != .locked {
85
+ // 2. Reset Exposure to the center ONLY if it is not explicitly locked by the user
86
+ if self.shouldAutoAdjustExposureOnFocus {
87
87
  if device.isExposureModeSupported(.continuousAutoExposure) {
88
88
  device.exposureMode = .continuousAutoExposure
89
89
  if device.isExposurePointOfInterestSupported {
@@ -105,6 +105,14 @@ class CameraController: NSObject {
105
105
 
106
106
  var captureSession: AVCaptureSession?
107
107
  var disableFocusIndicator: Bool = false
108
+ /// App-level exposure preference. AVFoundation's `.autoExpose` auto-transitions to
109
+ /// `.locked` after one-shot metering, so device.exposureMode alone cannot tell whether
110
+ /// the user explicitly locked exposure via setExposureMode("LOCK").
111
+ private var preferredExposureMode: String = "CONTINUOUS"
112
+ /// AUTO/CONTINUOUS allow tap/restore to reset metering + EV; LOCK/CUSTOM must keep user settings.
113
+ private var shouldAutoAdjustExposureOnFocus: Bool {
114
+ preferredExposureMode == "AUTO" || preferredExposureMode == "CONTINUOUS"
115
+ }
108
116
  private var focusExposureRestoreGeneration: UInt = 0
109
117
  private var focusExposureRestoreTimeoutWorkItem: DispatchWorkItem?
110
118
  private let focusExposureRestoreTimeout: TimeInterval = 3.0
@@ -1290,12 +1298,13 @@ extension CameraController {
1290
1298
  self.rearCameraInput = newInput
1291
1299
  self.currentCameraPosition = .rear
1292
1300
  }
1293
- // (Lightweight focus config; non-fatal on failure)
1301
+ // (Lightweight focus/exposure config; non-fatal on failure)
1294
1302
  try? targetDevice.lockForConfiguration()
1295
1303
  if targetDevice.isFocusModeSupported(.continuousAutoFocus) {
1296
1304
  targetDevice.focusMode = .continuousAutoFocus
1297
1305
  }
1298
1306
  targetDevice.unlockForConfiguration()
1307
+ try? self.applyPreferredExposureMode(to: targetDevice)
1299
1308
 
1300
1309
  // Restore audio input if it existed
1301
1310
  if let audioInput = existingAudioInput, captureSession.canAddInput(audioInput) {
@@ -2224,7 +2233,8 @@ extension CameraController {
2224
2233
  device.focusPointOfInterest = point
2225
2234
  }
2226
2235
 
2227
- if adjustExposure, device.exposureMode != .locked {
2236
+ // Prefer app-level mode: `.autoExpose` leaves device.exposureMode as `.locked`.
2237
+ if adjustExposure, self.shouldAutoAdjustExposureOnFocus {
2228
2238
  if device.isExposurePointOfInterestSupported, device.isExposureModeSupported(.autoExpose) {
2229
2239
  device.exposureMode = .autoExpose
2230
2240
  device.setExposureTargetBias(0.0) { _ in }
@@ -2282,7 +2292,9 @@ extension CameraController {
2282
2292
  }
2283
2293
  }
2284
2294
 
2285
- if device.exposureMode != .locked {
2295
+ // Restore continuous AE for AUTO/CONTINUOUS only (preserve LOCK/CUSTOM).
2296
+ // Do not trust device.exposureMode here: one-shot `.autoExpose` ends in `.locked`.
2297
+ if self.shouldAutoAdjustExposureOnFocus {
2286
2298
  if device.isExposureModeSupported(.continuousAutoExposure) {
2287
2299
  device.exposureMode = .continuousAutoExposure
2288
2300
  if device.isExposurePointOfInterestSupported {
@@ -2302,13 +2314,7 @@ extension CameraController {
2302
2314
  guard let device = self.activeVideoDevice() else { return }
2303
2315
 
2304
2316
  let centerPoint = CGPoint(x: 0.5, y: 0.5)
2305
- let adjustExposure: Bool
2306
- do {
2307
- let exposureMode = try self.getExposureMode()
2308
- adjustExposure = exposureMode == "AUTO" || exposureMode == "CONTINUOUS"
2309
- } catch {
2310
- adjustExposure = true
2311
- }
2317
+ let adjustExposure = self.shouldAutoAdjustExposureOnFocus
2312
2318
 
2313
2319
  do {
2314
2320
  try self.applyOneShotFocusAndExposure(at: centerPoint, on: device, adjustExposure: adjustExposure)
@@ -2498,14 +2504,15 @@ extension CameraController {
2498
2504
  self.rearCameraInput = newInput
2499
2505
  self.rearCamera = targetDevice
2500
2506
  self.currentCameraPosition = .rear
2507
+ }
2501
2508
 
2502
- // Configure rear camera
2503
- try targetDevice.lockForConfiguration()
2504
- if targetDevice.isFocusModeSupported(.continuousAutoFocus) {
2505
- targetDevice.focusMode = .continuousAutoFocus
2506
- }
2507
- targetDevice.unlockForConfiguration()
2509
+ // Lightweight focus/exposure config; non-fatal on failure
2510
+ try? targetDevice.lockForConfiguration()
2511
+ if targetDevice.isFocusModeSupported(.continuousAutoFocus) {
2512
+ targetDevice.focusMode = .continuousAutoFocus
2508
2513
  }
2514
+ targetDevice.unlockForConfiguration()
2515
+ try? self.applyPreferredExposureMode(to: targetDevice)
2509
2516
  } else {
2510
2517
  throw CameraControllerError.invalidOperation
2511
2518
  }
@@ -2564,6 +2571,7 @@ extension CameraController {
2564
2571
 
2565
2572
  self.captureSession = nil
2566
2573
  self.currentCameraPosition = nil
2574
+ self.preferredExposureMode = "CONTINUOUS"
2567
2575
 
2568
2576
  // Reset output preparation status
2569
2577
  self.outputsPrepared = false
@@ -2576,6 +2584,39 @@ extension CameraController {
2576
2584
 
2577
2585
  // MARK: - Exposure Controls
2578
2586
 
2587
+ private func applyPreferredExposureMode(to device: AVCaptureDevice) throws {
2588
+ let mode = self.preferredExposureMode
2589
+ let desiredMode: AVCaptureDevice.ExposureMode
2590
+ switch mode {
2591
+ case "LOCK":
2592
+ desiredMode = .locked
2593
+ case "AUTO":
2594
+ desiredMode = .autoExpose
2595
+ case "CUSTOM":
2596
+ desiredMode = .custom
2597
+ default:
2598
+ desiredMode = .continuousAutoExposure
2599
+ }
2600
+
2601
+ guard device.isExposureModeSupported(desiredMode) else {
2602
+ // Fall back to continuous when the preferred mode is unavailable on this lens.
2603
+ if device.isExposureModeSupported(.continuousAutoExposure) {
2604
+ try device.lockForConfiguration()
2605
+ device.exposureMode = .continuousAutoExposure
2606
+ device.setExposureTargetBias(0.0) { _ in }
2607
+ device.unlockForConfiguration()
2608
+ }
2609
+ return
2610
+ }
2611
+
2612
+ try device.lockForConfiguration()
2613
+ device.exposureMode = desiredMode
2614
+ if desiredMode == .autoExpose || desiredMode == .continuousAutoExposure {
2615
+ device.setExposureTargetBias(0.0) { _ in }
2616
+ }
2617
+ device.unlockForConfiguration()
2618
+ }
2619
+
2579
2620
  func getExposureModes() throws -> [String] {
2580
2621
  var currentCamera: AVCaptureDevice?
2581
2622
  switch currentCameraPosition {
@@ -2610,22 +2651,12 @@ extension CameraController {
2610
2651
  break
2611
2652
  }
2612
2653
 
2613
- guard let device = currentCamera else {
2654
+ guard currentCamera != nil else {
2614
2655
  throw CameraControllerError.noCamerasAvailable
2615
2656
  }
2616
2657
 
2617
- switch device.exposureMode {
2618
- case .locked:
2619
- return "LOCK"
2620
- case .autoExpose:
2621
- return "AUTO"
2622
- case .continuousAutoExposure:
2623
- return "CONTINUOUS"
2624
- case .custom:
2625
- return "CUSTOM"
2626
- @unknown default:
2627
- return "CONTINUOUS"
2628
- }
2658
+ // App preference, not device.exposureMode: one-shot `.autoExpose` ends as `.locked`.
2659
+ return self.preferredExposureMode
2629
2660
  }
2630
2661
 
2631
2662
  func setExposureMode(mode: String) throws {
@@ -2662,14 +2693,15 @@ extension CameraController {
2662
2693
  throw CameraControllerError.invalidOperation
2663
2694
  }
2664
2695
 
2696
+ switch normalized {
2697
+ case "LOCK", "AUTO", "CONTINUOUS", "CUSTOM":
2698
+ self.preferredExposureMode = normalized
2699
+ default:
2700
+ self.preferredExposureMode = "CONTINUOUS"
2701
+ }
2702
+
2665
2703
  do {
2666
- try device.lockForConfiguration()
2667
- device.exposureMode = finalMode
2668
- // Reset EV to 0 when switching to AUTO or CONTINUOUS
2669
- if finalMode == .autoExpose || finalMode == .continuousAutoExposure {
2670
- device.setExposureTargetBias(0.0) { _ in }
2671
- }
2672
- device.unlockForConfiguration()
2704
+ try self.applyPreferredExposureMode(to: device)
2673
2705
  } catch {
2674
2706
  throw CameraControllerError.invalidOperation
2675
2707
  }
@@ -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.11.1"
38
+ private let pluginVersion: String = "8.11.3"
39
39
  public let identifier = "CameraPreviewPlugin"
40
40
  public let jsName = "CameraPreview"
41
41
  public let pluginMethods: [CAPPluginMethod] = [
@@ -1233,7 +1233,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1233
1233
  print("[CameraPreview] Location permission not determined, requesting...")
1234
1234
  // Save the call for the delegate callback
1235
1235
  print("[CameraPreview] Saving call for location authorization flow")
1236
- self.bridge?.saveCall(call)
1236
+ call.keepAlive = true
1237
1237
  self.permissionCallID = call.callbackId
1238
1238
  self.waitingForLocation = true
1239
1239
 
@@ -2029,14 +2029,14 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
2029
2029
  print("[CameraPreview] Location authorized, getting location for capture")
2030
2030
  self.getCurrentLocation { _ in
2031
2031
  self.performCapture(call: call)
2032
- self.bridge?.releaseCall(call)
2032
+ call.keepAlive = false
2033
2033
  self.permissionCallID = nil
2034
2034
  self.waitingForLocation = false
2035
2035
  }
2036
2036
  case .denied, .restricted:
2037
2037
  print("[CameraPreview] Location denied, rejecting capture")
2038
2038
  call.reject("Location permission denied")
2039
- self.bridge?.releaseCall(call)
2039
+ call.keepAlive = false
2040
2040
  self.permissionCallID = nil
2041
2041
  self.waitingForLocation = false
2042
2042
  case .notDetermined:
@@ -2045,7 +2045,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
2045
2045
  @unknown default:
2046
2046
  print("[CameraPreview] Unknown status, rejecting capture")
2047
2047
  call.reject("Unknown location permission status")
2048
- self.bridge?.releaseCall(call)
2048
+ call.keepAlive = false
2049
2049
  self.permissionCallID = nil
2050
2050
  self.waitingForLocation = false
2051
2051
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.11.1",
3
+ "version": "8.11.3",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {
@@ -56,6 +56,7 @@
56
56
  "watch": "tsc --watch",
57
57
  "prepublishOnly": "bun run build",
58
58
  "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs",
59
+ "check:cap9-deprecated": "bun scripts/check-cap9-deprecated.mjs",
59
60
  "example:install": "cd example-app && bun install --frozen-lockfile",
60
61
  "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
61
62
  "changelog:ai": "node scripts/generate-ai-changelog.mjs"