@capgo/camera-preview 8.11.2 → 8.11.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.
@@ -53,13 +53,13 @@ dependencies {
53
53
  implementation project(':capacitor-android')
54
54
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
55
55
  implementation 'androidx.exifinterface:exifinterface:1.4.2'
56
- implementation 'com.google.android.gms:play-services-location:21.3.0'
56
+ implementation 'com.google.android.gms:play-services-location:21.4.0'
57
57
  implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
58
58
  // In-memory EXIF writing (no temp files)
59
59
  implementation 'org.apache.commons:commons-imaging:1.0.0-alpha6'
60
60
 
61
61
  // CameraX dependencies
62
- def camerax_version = "1.6.1"
62
+ def camerax_version = "1.6.2"
63
63
  implementation "androidx.camera:camera-core:${camerax_version}"
64
64
  implementation "androidx.camera:camera-camera2:${camerax_version}"
65
65
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
@@ -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
+ }
@@ -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.2"
38
+ private let pluginVersion: String = "8.11.4"
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.2",
3
+ "version": "8.11.4",
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"