@capacitor-plus/android 8.5.3 → 8.6.1

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.
@@ -15,14 +15,39 @@
15
15
 
16
16
  -keep public class * extends com.getcapacitor.Plugin { *; }
17
17
 
18
+ # The annotation types are read at runtime via reflection. If they are not kept, R8 full mode
19
+ # treats them as never instantiated and optimizes the annotation cached in PluginHandle to null,
20
+ # even with -keepattributes *Annotation*.
21
+ -keep @interface com.getcapacitor.annotation.** { *; }
22
+ -keep @interface com.getcapacitor.PluginMethod { *; }
23
+
24
+ # Callback methods are found by reflection, including on non-public base classes.
25
+ -keepclassmembers class * extends com.getcapacitor.Plugin {
26
+ @com.getcapacitor.annotation.ActivityCallback <methods>;
27
+ @com.getcapacitor.annotation.PermissionCallback <methods>;
28
+ }
29
+
18
30
  # Rules for Capacitor v2 plugins and annotations
19
31
  # These are deprecated but can still be used with Capacitor for now
20
32
  -keep @com.getcapacitor.NativePlugin public class * {
21
33
  @com.getcapacitor.PluginMethod public <methods>;
22
34
  }
35
+ -keep @interface com.getcapacitor.NativePlugin { *; }
23
36
 
24
37
  # Rules for Cordova plugins
25
38
  -keep public class * extends org.apache.cordova.* {
26
39
  public <methods>;
27
40
  public <fields>;
41
+ }
42
+
43
+ # CapacitorHttp loads these via reflection when an SSL pinning plugin is present.
44
+ -keep class io.ionic.sslpinning.SSLPinning {
45
+ <init>();
46
+ public javax.net.ssl.SSLSocketFactory getSSLSocketFactory(com.getcapacitor.Bridge);
47
+ public boolean isDomainExcluded(com.getcapacitor.Bridge, java.net.URL);
48
+ }
49
+ -keep class com.getcapacitor.community.sslpinning.SSLPinning {
50
+ <init>();
51
+ public javax.net.ssl.SSLSocketFactory getSSLSocketFactory(com.getcapacitor.Bridge);
52
+ public boolean isDomainExcluded(com.getcapacitor.Bridge, java.net.URL);
28
53
  }
@@ -417,12 +417,6 @@ var nativeBridge = (function (exports) {
417
417
  }
418
418
  };
419
419
  const platform = getPlatformId(win);
420
- if (platform == 'android' && typeof win.CapacitorSystemBarsAndroidInterface !== 'undefined') {
421
- // add DOM ready listener for System Bars
422
- document.addEventListener('DOMContentLoaded', function () {
423
- win.CapacitorSystemBarsAndroidInterface.onDOMReady();
424
- });
425
- }
426
420
  if (platform == 'android' || platform == 'ios') {
427
421
  // patch document.cookie on Android/iOS
428
422
  win.CapacitorCookiesDescriptor =
@@ -393,7 +393,39 @@ public class Bridge {
393
393
  return 0;
394
394
  }
395
395
 
396
+ private static int effectivePort(Uri uri) {
397
+ int port = uri.getPort();
398
+ if (port != -1) {
399
+ return port;
400
+ }
401
+ if ("https".equals(uri.getScheme())) {
402
+ return 443;
403
+ }
404
+ if ("http".equals(uri.getScheme())) {
405
+ return 80;
406
+ }
407
+ return -1;
408
+ }
409
+
410
+ public static boolean shouldBlockHttpInterceptorNavigation(Uri url, Uri appOrigin) {
411
+ String path = url.getPath();
412
+ if (path == null || !path.startsWith(CAPACITOR_HTTP_INTERCEPTOR_START)) {
413
+ return false;
414
+ }
415
+ return (
416
+ appOrigin.getHost().equals(url.getHost()) &&
417
+ url.getScheme().equals(appOrigin.getScheme()) &&
418
+ effectivePort(appOrigin) == effectivePort(url)
419
+ );
420
+ }
421
+
396
422
  public boolean launchIntent(Uri url) {
423
+ // The proxy returns a remote body at the app origin, so block it before plugins can allow it.
424
+ Uri appUri = Uri.parse(appUrl);
425
+ if (shouldBlockHttpInterceptorNavigation(url, appUri)) {
426
+ return true;
427
+ }
428
+
397
429
  /*
398
430
  * Give plugins the chance to handle the url
399
431
  */
@@ -411,7 +443,6 @@ public class Bridge {
411
443
  return false;
412
444
  }
413
445
 
414
- Uri appUri = Uri.parse(appUrl);
415
446
  if (
416
447
  !(appUri.getHost().equals(url.getHost()) && url.getScheme().equals(appUri.getScheme())) &&
417
448
  !appAllowNavigationMask.matches(url.getHost())
@@ -1262,6 +1293,10 @@ public class Bridge {
1262
1293
  }
1263
1294
  }
1264
1295
  }
1296
+ } else {
1297
+ Logger.warn(
1298
+ String.format("getPermissionStates: missing @CapacitorPlugin annotation for plugin %s", plugin.getPluginHandle().getId())
1299
+ );
1265
1300
  }
1266
1301
 
1267
1302
  return permissionsResults;
@@ -412,12 +412,9 @@ public class BridgeWebChromeClient extends WebChromeClient {
412
412
  }
413
413
  takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri);
414
414
  takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
415
-
416
- // Store in static variables to survive activity recreation
417
415
  pendingFilePathCallback = filePathCallback;
418
416
  pendingImageFileUri = imageFileUri;
419
417
  pendingFileChooserType = FileChooserType.IMAGE_CAPTURE;
420
-
421
418
  activityListener = (activityResult) -> {
422
419
  Uri[] result = null;
423
420
  if (activityResult.getResultCode() == Activity.RESULT_OK) {
@@ -485,6 +485,13 @@ public class Plugin {
485
485
  return;
486
486
  }
487
487
 
488
+ if (handle.getPluginAnnotation() == null) {
489
+ String error = String.format("Cannot request permissions for plugin %s without a @CapacitorPlugin annotation", handle.getId());
490
+ Logger.error(error);
491
+ call.reject(error);
492
+ return;
493
+ }
494
+
488
495
  String[] permissions = getPermissionStringsForAliases(aliases);
489
496
 
490
497
  if (permissions.length > 0) {
@@ -502,10 +509,14 @@ public class Plugin {
502
509
  private String[] getPermissionStringsForAliases(@NonNull String[] aliases) {
503
510
  CapacitorPlugin annotation = handle.getPluginAnnotation();
504
511
  HashSet<String> perms = new HashSet<>();
505
- for (Permission perm : annotation.permissions()) {
506
- if (Arrays.asList(aliases).contains(perm.alias())) {
507
- perms.addAll(Arrays.asList(perm.strings()));
512
+ if (annotation != null) {
513
+ for (Permission perm : annotation.permissions()) {
514
+ if (Arrays.asList(aliases).contains(perm.alias())) {
515
+ perms.addAll(Arrays.asList(perm.strings()));
516
+ }
508
517
  }
518
+ } else {
519
+ Logger.warn(String.format("getPermissionStringsForAliases: missing @CapacitorPlugin annotation for plugin %s", handle.getId()));
509
520
  }
510
521
 
511
522
  return perms.toArray(new String[0]);
@@ -1003,7 +1014,8 @@ public class Plugin {
1003
1014
  * Give the plugins a chance to take control when a URL is about to be loaded in the WebView.
1004
1015
  * Returning true causes the WebView to abort loading the URL.
1005
1016
  * Returning false causes the WebView to continue loading the URL.
1006
- * Returning null will defer to the default Capacitor policy
1017
+ * Returning null will defer to the default Capacitor policy.
1018
+ * Not called for Capacitor's internal HTTP proxy path, which is always blocked.
1007
1019
  */
1008
1020
  @SuppressWarnings("unused")
1009
1021
  public Boolean shouldOverrideLoad(Uri url) {
@@ -185,6 +185,11 @@ public class WebViewLocalServer {
185
185
  Uri loadingUrl = request.getUrl();
186
186
 
187
187
  if (null != loadingUrl.getPath() && loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) {
188
+ // Only fetch/XHR should reach the proxy; a document would run remote content at the app origin.
189
+ boolean httpEnabled = bridge.getConfig().getPluginConfiguration("CapacitorHttp").getBoolean("enabled", false);
190
+ if (!httpEnabled || isDocumentRequest(request)) {
191
+ return null;
192
+ }
188
193
  Logger.debug("Handling CapacitorHttp request: " + loadingUrl);
189
194
  try {
190
195
  return handleCapacitorHttpRequest(request);
@@ -210,6 +215,24 @@ public class WebViewLocalServer {
210
215
  }
211
216
  }
212
217
 
218
+ /** isForMainFrame() is false for an iframe and a fetch alike; only navigations send this header. */
219
+ private boolean isDocumentRequest(WebResourceRequest request) {
220
+ if (request.isForMainFrame()) {
221
+ return true;
222
+ }
223
+ Map<String, String> headers = request.getRequestHeaders();
224
+ if (headers == null) {
225
+ // The proxy needs the headers too, so the request fails there anyway.
226
+ return false;
227
+ }
228
+ for (String header : headers.keySet()) {
229
+ if ("Upgrade-Insecure-Requests".equalsIgnoreCase(header)) {
230
+ return true;
231
+ }
232
+ }
233
+ return false;
234
+ }
235
+
213
236
  private boolean isLocalFile(Uri uri) {
214
237
  String path = uri.getPath();
215
238
  return path.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart);
@@ -333,6 +356,9 @@ public class WebViewLocalServer {
333
356
  int responseCode = connection.getResponseCode();
334
357
  String reasonPhrase = getReasonPhraseFromResponseCode(responseCode);
335
358
 
359
+ // Nothing should render this. If anything does, sandbox keeps it inert and off the app origin.
360
+ responseHeaders.put("Content-Security-Policy", "sandbox; frame-ancestors 'none'");
361
+
336
362
  return new WebResourceResponse(mimeType, encoding, responseCode, reasonPhrase, responseHeaders, inputStream);
337
363
  }
338
364
 
@@ -756,9 +782,7 @@ public class WebViewLocalServer {
756
782
  }
757
783
  }
758
784
 
759
- /**
760
- * An InputStream wrapper that limits the number of bytes that can be read.
761
- */
785
+ // For L and above.
762
786
  static class BoundedInputStream extends InputStream {
763
787
 
764
788
  private final InputStream in;
@@ -192,7 +192,14 @@ public class MockCordovaWebViewImpl implements CordovaWebView {
192
192
  }
193
193
 
194
194
  public void triggerDocumentEvent(final String eventName) {
195
- eval("window.Capacitor.triggerEvent('" + eventName + "', 'document');", (s) -> {});
195
+ // Pages served without the bridge (e.g. server.errorPath) have no window.Capacitor; drop the event rather than throw
196
+ eval(
197
+ "if (window.Capacitor != null && typeof window.Capacitor.triggerEvent === 'function') {" +
198
+ "window.Capacitor.triggerEvent('" +
199
+ eventName +
200
+ "', 'document'); }",
201
+ (s) -> {}
202
+ );
196
203
  }
197
204
 
198
205
  @Override
@@ -9,7 +9,6 @@ import android.os.Build;
9
9
  import android.util.TypedValue;
10
10
  import android.view.View;
11
11
  import android.view.Window;
12
- import android.webkit.JavascriptInterface;
13
12
  import android.webkit.WebView;
14
13
  import androidx.core.graphics.Insets;
15
14
  import androidx.core.view.ViewCompat;
@@ -37,6 +36,7 @@ public class SystemBars extends Plugin {
37
36
  // TODO: In Cap 9, add an additional option "full"
38
37
  static final String INSETS_HANDLING_CSS = "css";
39
38
  static final String INSETS_HANDLING_DISABLE = "disable";
39
+ static final String INSETS_HANDLING_NATIVE = "native";
40
40
 
41
41
  // https://issues.chromium.org/issues/40699457
42
42
  private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140;
@@ -62,11 +62,22 @@ public class SystemBars extends Plugin {
62
62
  private String currentStatusBarStyle = STYLE_DEFAULT;
63
63
  private String currentGestureBarStyle = STYLE_DEFAULT;
64
64
 
65
- private boolean navBarVisible = true;
65
+ // Declare variable at this scope to help prevent adding multiple listeners.
66
+ private WebViewListener webViewListener;
67
+
68
+ private void warnAboutUnsupportedConfigurationValues() {
69
+ boolean keyboardResizeOnFullScreen = bridge.getConfig().getPluginConfiguration("Keyboard").getBoolean("resizeOnFullScreen", false);
70
+
71
+ if (!INSETS_HANDLING_DISABLE.equals(insetsHandling) && keyboardResizeOnFullScreen) {
72
+ Logger.warn(
73
+ "SystemBars",
74
+ "You should omit `Keyboard.resizeOnFullScreen` in your `capacitor.config.json`. Other values can lead to unexpected behavior."
75
+ );
76
+ }
77
+ }
66
78
 
67
79
  @Override
68
80
  public void load() {
69
- getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface");
70
81
  super.load();
71
82
 
72
83
  initSystemBars();
@@ -76,23 +87,27 @@ public class SystemBars extends Plugin {
76
87
  protected void handleOnStart() {
77
88
  super.handleOnStart();
78
89
 
79
- this.getBridge().addWebViewListener(
80
- new WebViewListener() {
90
+ if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
91
+ return;
92
+ }
93
+
94
+ if (webViewListener == null) {
95
+ webViewListener = new WebViewListener() {
81
96
  @Override
82
97
  public void onPageCommitVisible(WebView view, String url) {
83
98
  super.onPageCommitVisible(view, url);
84
- View parentView = (View) getBridge().getWebView().getParent();
85
- ViewCompat.requestApplyInsets(parentView);
86
- if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
87
- WindowInsetsCompat rootInsets = ViewCompat.getRootWindowInsets(parentView);
88
- if (rootInsets != null) {
89
- Insets safeArea = calcSafeAreaInsets(rootInsets);
90
- injectSafeAreaCSS(safeArea.top, safeArea.right, safeArea.bottom, safeArea.left);
91
- }
92
- }
99
+ bridge
100
+ .getWebView()
101
+ .evaluateJavascript(viewportMetaJSFunction, (res) -> {
102
+ hasViewportCover = "true".equals(res);
103
+
104
+ // Request new execution tree of `setOnApplyWindowInsetsListener`
105
+ bridge.getWebView().requestApplyInsets();
106
+ });
93
107
  }
94
- }
95
- );
108
+ };
109
+ this.getBridge().addWebViewListener(webViewListener);
110
+ }
96
111
  }
97
112
 
98
113
  @Override
@@ -104,11 +119,20 @@ public class SystemBars extends Plugin {
104
119
  }
105
120
 
106
121
  private void initSystemBars() {
122
+ // If you already know what the value of the `viewport-fit=` meta tag is going to be,
123
+ // passing it here through `initialViewportFitValueHint` can help prevent layout shifting.
124
+ String configuredInitialViewportFitValueHint = getConfig().getString("initialViewportFitValueHint", "");
125
+ hasViewportCover = "cover".equals(configuredInitialViewportFitValueHint);
126
+
107
127
  String style = getConfig().getString("style", STYLE_DEFAULT).toUpperCase(Locale.US);
108
128
  boolean hidden = getConfig().getBoolean("hidden", false);
109
129
 
110
130
  String configuredInsetsHandling = getConfig().getString("insetsHandling", INSETS_HANDLING_CSS);
111
- if (INSETS_HANDLING_CSS.equals(configuredInsetsHandling) || INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling)) {
131
+ if (
132
+ INSETS_HANDLING_CSS.equals(configuredInsetsHandling) ||
133
+ INSETS_HANDLING_DISABLE.equals(configuredInsetsHandling) ||
134
+ INSETS_HANDLING_NATIVE.equals(configuredInsetsHandling)
135
+ ) {
112
136
  insetsHandling = configuredInsetsHandling;
113
137
  } else {
114
138
  Logger.warn(
@@ -118,8 +142,9 @@ public class SystemBars extends Plugin {
118
142
  insetsHandling = INSETS_HANDLING_CSS;
119
143
  }
120
144
 
145
+ warnAboutUnsupportedConfigurationValues();
146
+
121
147
  initWindowInsetsListener();
122
- initSafeAreaCSSVariables();
123
148
 
124
149
  getBridge().executeOnMainThread(() -> {
125
150
  Window window = getActivity().getWindow();
@@ -172,96 +197,14 @@ public class SystemBars extends Plugin {
172
197
  call.resolve();
173
198
  }
174
199
 
175
- @Override
176
- protected void handleOnResume() {
177
- super.handleOnResume();
178
- getBridge().executeOnMainThread(() -> {
179
- Window window = getActivity().getWindow();
180
- WindowCompat.setDecorFitsSystemWindows(window, false);
181
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
182
- window.setNavigationBarColor(android.graphics.Color.TRANSPARENT);
183
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
184
- window.setNavigationBarContrastEnforced(false);
185
- }
186
- }
187
- setStyle(currentGestureBarStyle, BAR_GESTURE_BAR);
188
- setStyle(currentStatusBarStyle, BAR_STATUS_BAR);
189
- ViewCompat.requestApplyInsets((View) getBridge().getWebView().getParent());
190
- });
191
- }
192
-
193
- @JavascriptInterface
194
- public void onDOMReady() {
195
- if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
196
- getActivity().runOnUiThread(() -> {
197
- this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> {
198
- hasViewportCover = res.equals("true");
199
-
200
- getBridge().getWebView().requestApplyInsets();
201
- });
202
- });
203
- }
204
- }
205
-
206
- private Insets calcSafeAreaInsets(WindowInsetsCompat insets) {
207
- Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
208
-
209
- int bottom = safeArea.bottom;
210
-
211
- if (bottom == 0 && Build.VERSION.SDK_INT < Build.VERSION_CODES.R && safeArea.left == 0 && safeArea.right == 0) {
212
- // skip if nav bar is on a side (landscape)
213
- bottom = getNavBarHeightFromResources();
214
- }
215
-
216
- boolean imeVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
217
- ? insets.isVisible(WindowInsetsCompat.Type.ime())
218
- : insets.getInsets(WindowInsetsCompat.Type.ime()).bottom > 0;
219
-
220
- if (imeVisible) {
221
- return Insets.of(safeArea.left, safeArea.top, safeArea.right, 0);
222
- }
223
- return Insets.of(safeArea.left, safeArea.top, safeArea.right, bottom);
224
- }
225
-
226
- // Only reached on API < 30 when the window reports no inset on any edge, so the
227
- // choice is this approximation or a hardcoded 0. Lint rejects the internal
228
- // resource because OEMs may change it; a wrong height still beats no inset.
229
- @SuppressLint({ "InternalInsetResource", "DiscouragedApi" })
230
- private int getNavBarHeightFromResources() {
231
- if (!navBarVisible) return 0;
232
- android.content.res.Resources res = getActivity().getResources();
233
- int heightId = res.getIdentifier("navigation_bar_height", "dimen", "android");
234
- return heightId > 0 ? res.getDimensionPixelSize(heightId) : 0;
235
- }
236
-
237
- private void initSafeAreaCSSVariables() {
238
- if (INSETS_HANDLING_CSS.equals(insetsHandling)) {
239
- WindowInsetsCompat insets;
240
-
241
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
242
- View v = (View) this.getBridge().getWebView().getParent();
243
- insets = ViewCompat.getRootWindowInsets(v);
244
- } else {
245
- insets = WindowInsetsCompat.CONSUMED;
246
- }
247
-
248
- if (insets != null) {
249
- Insets safeAreaInsets = calcSafeAreaInsets(insets);
250
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
251
- }
252
- }
253
- }
254
-
255
200
  private void initWindowInsetsListener() {
256
201
  if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) {
257
202
  return;
258
203
  }
259
204
 
260
- ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> {
261
- // getRootWindowInsets() bypasses AppCompat's intermediate view consuming the bottom inset on API < 30
262
- WindowInsetsCompat rawInsets = ViewCompat.getRootWindowInsets(v);
263
- WindowInsetsCompat safeAreaSource = (rawInsets != null) ? rawInsets : insets;
205
+ View view = getActivity().getWindow().getDecorView();
264
206
 
207
+ ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
265
208
  boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover;
266
209
 
267
210
  Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
@@ -272,10 +215,7 @@ public class SystemBars extends Plugin {
272
215
  // We need to correct for a possible shown IME
273
216
  v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);
274
217
 
275
- Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource);
276
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
277
-
278
- return new WindowInsetsCompat.Builder(insets)
218
+ WindowInsetsCompat newInsets = new WindowInsetsCompat.Builder(insets)
279
219
  .setInsets(
280
220
  WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),
281
221
  Insets.of(
@@ -286,9 +226,19 @@ public class SystemBars extends Plugin {
286
226
  )
287
227
  )
288
228
  .build();
229
+
230
+ injectSafeAreaCSS(newInsets);
231
+
232
+ return newInsets;
289
233
  }
290
234
 
291
- v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0);
235
+ // We need to correct for a possible shown IME
236
+ v.setPadding(
237
+ systemBarsInsets.left,
238
+ systemBarsInsets.top,
239
+ systemBarsInsets.right,
240
+ keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom
241
+ );
292
242
 
293
243
  // Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
294
244
  // So we have to explicitly set insets to `0`
@@ -297,20 +247,28 @@ public class SystemBars extends Plugin {
297
247
  .setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0))
298
248
  .build();
299
249
 
300
- Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource);
301
- injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left);
250
+ injectSafeAreaCSS(insets);
302
251
 
303
252
  return newInsets;
304
253
  });
305
254
  }
306
255
 
307
- private void injectSafeAreaCSS(int top, int right, int bottom, int left) {
256
+ private void injectSafeAreaCSS(WindowInsetsCompat insets) {
257
+ if (!INSETS_HANDLING_CSS.equals(insetsHandling)) {
258
+ return;
259
+ }
260
+
261
+ Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
262
+ boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime());
263
+
308
264
  // Convert pixels to density-independent pixels
309
265
  float density = getActivity().getResources().getDisplayMetrics().density;
310
- float topPx = top / density;
311
- float rightPx = right / density;
312
- float bottomPx = bottom / density;
313
- float leftPx = left / density;
266
+ float topPx = systemBarsInsets.top / density;
267
+ float rightPx = systemBarsInsets.right / density;
268
+ // For native insets the value gets automatically corrected when the IME is visible (in newer WebView versions),
269
+ // but for these injected values we have to handle that manually (for all WebView versions).
270
+ float bottomPx = (keyboardVisible ? 0 : systemBarsInsets.bottom) / density;
271
+ float leftPx = systemBarsInsets.left / density;
314
272
 
315
273
  // Execute JavaScript to inject the CSS
316
274
  getBridge().executeOnMainThread(() -> {
@@ -364,24 +322,20 @@ public class SystemBars extends Plugin {
364
322
  if (hide) {
365
323
  if (bar.isEmpty()) {
366
324
  windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars());
367
- navBarVisible = false;
368
325
  } else if (bar.equals(BAR_STATUS_BAR)) {
369
326
  windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.statusBars());
370
327
  } else if (bar.equals(BAR_GESTURE_BAR)) {
371
328
  windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.navigationBars());
372
- navBarVisible = false;
373
329
  }
374
330
  return;
375
331
  }
376
332
 
377
333
  if (bar.isEmpty()) {
378
334
  windowInsetsControllerCompat.show(WindowInsetsCompat.Type.systemBars());
379
- navBarVisible = true;
380
335
  } else if (bar.equals(BAR_STATUS_BAR)) {
381
336
  windowInsetsControllerCompat.show(WindowInsetsCompat.Type.statusBars());
382
337
  } else if (bar.equals(BAR_GESTURE_BAR)) {
383
338
  windowInsetsControllerCompat.show(WindowInsetsCompat.Type.navigationBars());
384
- navBarVisible = true;
385
339
  }
386
340
  }
387
341
 
@@ -499,9 +499,12 @@ public class CapacitorHttpUrlConnection implements ICapacitorHttpUrlConnection {
499
499
  }
500
500
 
501
501
  public void setSSLSocketFactory(Bridge bridge) {
502
- // Attach SSL Certificates if Enterprise Plugin is available
502
+ // Attach SSL Certificates if an SSL Pinning plugin is available
503
503
  try {
504
- Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning");
504
+ Class<?> sslPinningImpl = findSSLPinningClass();
505
+ if (sslPinningImpl == null) {
506
+ return;
507
+ }
505
508
  Method method = sslPinningImpl.getDeclaredMethod("getSSLSocketFactory", Bridge.class);
506
509
  SSLSocketFactory sslSocketFactory = (SSLSocketFactory) method.invoke(
507
510
  sslPinningImpl.getDeclaredConstructor().newInstance(),
@@ -512,4 +515,13 @@ public class CapacitorHttpUrlConnection implements ICapacitorHttpUrlConnection {
512
515
  }
513
516
  } catch (Exception ignored) {}
514
517
  }
518
+
519
+ private static Class<?> findSSLPinningClass() {
520
+ for (String className : new String[] { "io.ionic.sslpinning.SSLPinning", "com.getcapacitor.community.sslpinning.SSLPinning" }) {
521
+ try {
522
+ return Class.forName(className);
523
+ } catch (ClassNotFoundException ignored) {}
524
+ }
525
+ return null;
526
+ }
515
527
  }
@@ -450,7 +450,10 @@ public class HttpRequestHandler {
450
450
 
451
451
  public static Boolean isDomainExcludedFromSSL(Bridge bridge, URL url) {
452
452
  try {
453
- Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning");
453
+ Class<?> sslPinningImpl = findSSLPinningClass();
454
+ if (sslPinningImpl == null) {
455
+ return false;
456
+ }
454
457
  Method method = sslPinningImpl.getDeclaredMethod("isDomainExcluded", Bridge.class, URL.class);
455
458
  return (Boolean) method.invoke(sslPinningImpl.getDeclaredConstructor().newInstance(), bridge, url);
456
459
  } catch (Exception ignored) {
@@ -458,6 +461,15 @@ public class HttpRequestHandler {
458
461
  }
459
462
  }
460
463
 
464
+ private static Class<?> findSSLPinningClass() {
465
+ for (String className : new String[] { "io.ionic.sslpinning.SSLPinning", "com.getcapacitor.community.sslpinning.SSLPinning" }) {
466
+ try {
467
+ return Class.forName(className);
468
+ } catch (ClassNotFoundException ignored) {}
469
+ }
470
+ return null;
471
+ }
472
+
461
473
  @FunctionalInterface
462
474
  public interface ProgressEmitter {
463
475
  void emit(Integer bytes, Integer contentLength);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capacitor-plus/android",
3
- "version": "8.5.3",
3
+ "version": "8.6.1",
4
4
  "description": "Capacitor+: Enhanced Capacitor with automated upstream sync - Cross-platform apps with JavaScript and the web",
5
5
  "homepage": "https://capgo.app/docs/plugins/capacitor-plus/",
6
6
  "author": "Capgo Team <support@capgo.app> (https://capgo.app)",
@@ -23,8 +23,8 @@
23
23
  "verify": "./gradlew clean lint build test -b capacitor/build.gradle"
24
24
  },
25
25
  "peerDependencies": {
26
- "@capacitor-plus/core": "^8.5.0",
27
- "@capacitor/core": "^8.5.0"
26
+ "@capacitor-plus/core": "^8.6.0",
27
+ "@capacitor/core": "^8.6.0"
28
28
  },
29
29
  "publishConfig": {
30
30
  "access": "public"