From 54b69ede9c3ca03045b9ad3a28815ed70c46daa7 Mon Sep 17 00:00:00 2001 From: Fabio Martino Date: Sun, 1 Mar 2026 21:18:22 +0100 Subject: [PATCH 1/3] fix(android): implement automatic edge-to-edge and universal keyboard handling --- .../com/getcapacitor/plugin/SystemBars.java | 112 +++++++++++++++--- 1 file changed, 93 insertions(+), 19 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java index 3f04adf97..0919acda3 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -1,5 +1,6 @@ package com.getcapacitor.plugin; +import android.app.Activity; import android.content.Context; import android.content.pm.PackageInfo; import android.content.res.Configuration; @@ -27,6 +28,9 @@ @CapacitorPlugin public class SystemBars extends Plugin { + private static final int MIN_INSETS_VERSION = Build.VERSION_CODES.LOLLIPOP; + private static final int BROKEN_WEBVIEW_VERSION = 140; + static final String STYLE_LIGHT = "LIGHT"; static final String STYLE_DARK = "DARK"; static final String STYLE_DEFAULT = "DEFAULT"; @@ -56,10 +60,24 @@ function capacitorSystemBarsCheckMetaViewport() { @Override public void load() { + enableEdgeToEdgeIfNeeded(); + getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface"); super.load(); initSystemBars(); + + getBridge().executeOnMainThread(() -> { + getBridge().getWebView().requestApplyInsets(); + }); + } + + private void enableEdgeToEdgeIfNeeded() { + try { + Class edgeToEdgeClass = Class.forName("androidx.activity.EdgeToEdge"); + java.lang.reflect.Method enableMethod = edgeToEdgeClass.getMethod("enable", Activity.class); + enableMethod.invoke(null, getActivity()); + } catch (Exception ignored) {} } @Override @@ -77,6 +95,12 @@ public void onPageCommitVisible(WebView view, String url) { ); } + @Override + protected void handleOnResume() { + super.handleOnResume(); + getBridge().executeOnMainThread(() -> getBridge().getWebView().requestApplyInsets()); + } + @Override protected void handleOnConfigurationChanged(Configuration newConfig) { super.handleOnConfigurationChanged(newConfig); @@ -158,7 +182,7 @@ private Insets calcSafeAreaInsets(WindowInsetsCompat insets) { } private void initSafeAreaInsets() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && insetHandlingEnabled) { + if (Build.VERSION.SDK_INT >= MIN_INSETS_VERSION && insetHandlingEnabled) { View v = (View) this.getBridge().getWebView().getParent(); WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(v); if (insets != null) { @@ -169,34 +193,84 @@ private void initSafeAreaInsets() { } private void initWindowInsetsListener() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && insetHandlingEnabled) { - ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> { - boolean hasBrokenWebViewVersion = getWebViewMajorVersion() <= 139; - - if (hasViewportCover) { - Insets safeAreaInsets = calcSafeAreaInsets(insets); - injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); + if (Build.VERSION.SDK_INT >= MIN_INSETS_VERSION && insetHandlingEnabled) { + View parentView = (View) getBridge().getWebView().getParent(); + ViewCompat.setOnApplyWindowInsetsListener(parentView, (v, insets) -> { + boolean hasBrokenWebViewVersion = getWebViewMajorVersion() < BROKEN_WEBVIEW_VERSION; + + if (!hasViewportCover) { + resetViewBottomMargin(v); + injectSafeAreaCSSWithBottom(0, 0, 0, 0); + return insets; } + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); + Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); + int bottom = insets.isVisible(WindowInsetsCompat.Type.ime()) ? imeInsets.bottom : systemBars.bottom; + if (hasBrokenWebViewVersion) { - if (hasViewportCover && v.hasWindowFocus() && v.isShown()) { - boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); - if (keyboardVisible) { - Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); - setViewMargins(v, Insets.of(0, 0, 0, imeInsets.bottom)); - } else { - setViewMargins(v, Insets.NONE); - } - - return WindowInsetsCompat.CONSUMED; - } + setViewBottomMargin(v, bottom); + injectSafeAreaCSSWithBottom(systemBars.top, systemBars.right, 0, systemBars.left); + return WindowInsetsCompat.CONSUMED; } + resetViewBottomMargin(v); + injectSafeAreaCSSWithBottom(systemBars.top, systemBars.right, bottom, systemBars.left); return insets; }); } } + private void setViewBottomMargin(View v, int bottom) { + ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); + mlp.leftMargin = 0; + mlp.bottomMargin = bottom; + mlp.rightMargin = 0; + mlp.topMargin = 0; + v.setLayoutParams(mlp); + } + + private void resetViewBottomMargin(View v) { + ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); + if (mlp.leftMargin != 0 || mlp.topMargin != 0 || mlp.rightMargin != 0 || mlp.bottomMargin != 0) { + mlp.leftMargin = 0; + mlp.topMargin = 0; + mlp.rightMargin = 0; + mlp.bottomMargin = 0; + v.setLayoutParams(mlp); + } + } + + private void injectSafeAreaCSSWithBottom(int top, int right, int bottom, int left) { + float density = getActivity().getResources().getDisplayMetrics().density; + float topPx = top / density; + float rightPx = right / density; + float bottomPx = bottom / density; + float leftPx = left / density; + + getBridge().executeOnMainThread(() -> { + if (bridge != null && bridge.getWebView() != null) { + String script = String.format( + Locale.US, + """ + try { + document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx"); + } catch(e) { console.error('Error injecting safe area CSS:', e); } + """, + (int) topPx, + (int) rightPx, + (int) bottomPx, + (int) leftPx + ); + + bridge.getWebView().evaluateJavascript(script, null); + } + }); + } + private void setViewMargins(View v, Insets insets) { ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) v.getLayoutParams(); mlp.leftMargin = insets.left; From 7cdf75df8c840fe62b80bf8a59561f2d70470d6d Mon Sep 17 00:00:00 2001 From: Fabio Martino Date: Mon, 9 Mar 2026 15:23:21 +0100 Subject: [PATCH 2/3] fix(android): fix excessive bottom safe area with keyboard --- .../com/getcapacitor/plugin/SystemBars.java | 106 ++++++++++-------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java index 0919acda3..3bae74373 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -1,6 +1,5 @@ package com.getcapacitor.plugin; -import android.app.Activity; import android.content.Context; import android.content.pm.PackageInfo; import android.content.res.Configuration; @@ -40,13 +39,10 @@ public class SystemBars extends Plugin { static final String INSETS_HANDLING_CSS = "css"; static final String INSETS_HANDLING_DISABLE = "disable"; - static final String viewportMetaJSFunction = """ + static final String VIEWPORT_META_JS = """ function capacitorSystemBarsCheckMetaViewport() { const meta = document.querySelectorAll("meta[name=viewport]"); - if (meta.length == 0) { - return false; - } - // get the last found meta viewport tag + if (meta.length == 0) return false; const metaContent = meta[meta.length - 1].content; return metaContent.includes("viewport-fit=cover"); } @@ -55,31 +51,23 @@ function capacitorSystemBarsCheckMetaViewport() { private boolean insetHandlingEnabled = true; private boolean hasViewportCover = false; + private Integer cachedWebViewMajorVersion; private String currentStyle = STYLE_DEFAULT; @Override public void load() { - enableEdgeToEdgeIfNeeded(); - getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface"); super.load(); initSystemBars(); getBridge().executeOnMainThread(() -> { + WindowCompat.setDecorFitsSystemWindows(getActivity().getWindow(), true); getBridge().getWebView().requestApplyInsets(); }); } - private void enableEdgeToEdgeIfNeeded() { - try { - Class edgeToEdgeClass = Class.forName("androidx.activity.EdgeToEdge"); - java.lang.reflect.Method enableMethod = edgeToEdgeClass.getMethod("enable", Activity.class); - enableMethod.invoke(null, getActivity()); - } catch (Exception ignored) {} - } - @Override protected void handleOnStart() { super.handleOnStart(); @@ -98,7 +86,10 @@ public void onPageCommitVisible(WebView view, String url) { @Override protected void handleOnResume() { super.handleOnResume(); - getBridge().executeOnMainThread(() -> getBridge().getWebView().requestApplyInsets()); + getBridge().executeOnMainThread(() -> { + WindowCompat.setDecorFitsSystemWindows(getActivity().getWindow(), true); + getBridge().getWebView().requestApplyInsets(); + }); } @Override @@ -113,7 +104,7 @@ private void initSystemBars() { boolean hidden = getConfig().getBoolean("hidden", false); String insetsHandling = getConfig().getString("insetsHandling", "css"); - if (insetsHandling.equals(INSETS_HANDLING_DISABLE)) { + if (INSETS_HANDLING_DISABLE.equals(insetsHandling)) { insetHandlingEnabled = false; } @@ -132,7 +123,7 @@ public void setStyle(final PluginCall call) { String style = call.getString("style", STYLE_DEFAULT); getBridge().executeOnMainThread(() -> { - setStyle(style, bar); + setStyle(style != null ? style : STYLE_DEFAULT, bar); call.resolve(); }); } @@ -164,13 +155,14 @@ public void setAnimation(final PluginCall call) { @JavascriptInterface public void onDOMReady() { - getActivity().runOnUiThread(() -> { - this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> { + getActivity().runOnUiThread(() -> + this.bridge.getWebView().evaluateJavascript(VIEWPORT_META_JS, (res) -> { hasViewportCover = res.equals("true"); + WindowCompat.setDecorFitsSystemWindows(getActivity().getWindow(), true); getBridge().getWebView().requestApplyInsets(); - }); - }); + }) + ); } private Insets calcSafeAreaInsets(WindowInsetsCompat insets) { @@ -195,30 +187,40 @@ private void initSafeAreaInsets() { private void initWindowInsetsListener() { if (Build.VERSION.SDK_INT >= MIN_INSETS_VERSION && insetHandlingEnabled) { View parentView = (View) getBridge().getWebView().getParent(); - ViewCompat.setOnApplyWindowInsetsListener(parentView, (v, insets) -> { - boolean hasBrokenWebViewVersion = getWebViewMajorVersion() < BROKEN_WEBVIEW_VERSION; + ViewCompat.setOnApplyWindowInsetsListener(parentView, this::applyInsets); + } + } - if (!hasViewportCover) { - resetViewBottomMargin(v); - injectSafeAreaCSSWithBottom(0, 0, 0, 0); - return insets; - } + private WindowInsetsCompat applyInsets(View v, WindowInsetsCompat insets) { + boolean hasBrokenWebViewVersion = getWebViewMajorVersion() < BROKEN_WEBVIEW_VERSION; - Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); - Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); - int bottom = insets.isVisible(WindowInsetsCompat.Type.ime()) ? imeInsets.bottom : systemBars.bottom; + Insets stableInsets = insets.getInsetsIgnoringVisibility( + WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout() + ); + Insets currentInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); - if (hasBrokenWebViewVersion) { - setViewBottomMargin(v, bottom); - injectSafeAreaCSSWithBottom(systemBars.top, systemBars.right, 0, systemBars.left); - return WindowInsetsCompat.CONSUMED; - } + boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); + + if (hasViewportCover) { + int topInset = stableInsets.top; + int bottomInset = keyboardVisible ? 0 : currentInsets.bottom; - resetViewBottomMargin(v); - injectSafeAreaCSSWithBottom(systemBars.top, systemBars.right, bottom, systemBars.left); - return insets; - }); + injectSafeAreaCSSWithBottom(topInset, currentInsets.right, bottomInset, currentInsets.left); } + + if (hasBrokenWebViewVersion && hasViewportCover && v.hasWindowFocus() && v.isShown()) { + if (keyboardVisible) { + Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); + setViewMargins(v, Insets.of(0, 0, 0, imeInsets.bottom)); + } else { + setViewMargins(v, Insets.NONE); + } + return WindowInsetsCompat.CONSUMED; + } + + resetViewBottomMargin(v); + + return insets; } private void setViewBottomMargin(View v, int bottom) { @@ -295,10 +297,10 @@ private void injectSafeAreaCSS(int top, int right, int bottom, int left) { Locale.US, """ try { - document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx"); } catch(e) { console.error('Error injecting safe area CSS:', e); } """, (int) topPx, @@ -370,13 +372,21 @@ public int getThemeColor(Context context, int attrRes) { return typedValue.data; } - private Integer getWebViewMajorVersion() { + private int getWebViewMajorVersion() { + if (cachedWebViewMajorVersion != null) { + return cachedWebViewMajorVersion; + } + PackageInfo info = WebViewCompat.getCurrentWebViewPackage(getContext()); if (info != null && info.versionName != null) { String[] versionSegments = info.versionName.split("\\."); - return Integer.valueOf(versionSegments[0]); + try { + cachedWebViewMajorVersion = Integer.valueOf(versionSegments[0]); + return cachedWebViewMajorVersion; + } catch (NumberFormatException ignored) {} } + cachedWebViewMajorVersion = 0; return 0; } } From 12270a3fdd3338d9fb8189699e25c9787ddbe638 Mon Sep 17 00:00:00 2001 From: Fabio Martino Date: Tue, 10 Mar 2026 15:30:49 +0100 Subject: [PATCH 3/3] feat(android): enhance SystemBars stability and keyboard handling --- .../com/getcapacitor/plugin/SystemBars.java | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java index 3bae74373..cd1468106 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -28,7 +28,9 @@ public class SystemBars extends Plugin { private static final int MIN_INSETS_VERSION = Build.VERSION_CODES.LOLLIPOP; - private static final int BROKEN_WEBVIEW_VERSION = 140; + + private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_FIX = 140; + private static final int WEBVIEW_VERSION_WITH_SAFE_AREA_KEYBOARD_FIX = 144; static final String STYLE_LIGHT = "LIGHT"; static final String STYLE_DARK = "DARK"; @@ -42,7 +44,10 @@ public class SystemBars extends Plugin { static final String VIEWPORT_META_JS = """ function capacitorSystemBarsCheckMetaViewport() { const meta = document.querySelectorAll("meta[name=viewport]"); - if (meta.length == 0) return false; + if (meta.length == 0) { + return false; + } + // get the last found meta viewport tag const metaContent = meta[meta.length - 1].content; return metaContent.includes("viewport-fit=cover"); } @@ -87,6 +92,7 @@ public void onPageCommitVisible(WebView view, String url) { protected void handleOnResume() { super.handleOnResume(); getBridge().executeOnMainThread(() -> { + // Ensure insets are requested when resuming WindowCompat.setDecorFitsSystemWindows(getActivity().getWindow(), true); getBridge().getWebView().requestApplyInsets(); }); @@ -192,14 +198,14 @@ private void initWindowInsetsListener() { } private WindowInsetsCompat applyInsets(View v, WindowInsetsCompat insets) { - boolean hasBrokenWebViewVersion = getWebViewMajorVersion() < BROKEN_WEBVIEW_VERSION; + int webViewVersion = getWebViewMajorVersion(); + boolean hasBrokenWebViewVersion = webViewVersion < WEBVIEW_VERSION_WITH_SAFE_AREA_FIX; + boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); Insets stableInsets = insets.getInsetsIgnoringVisibility( - WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout() - ); - Insets currentInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); - - boolean keyboardVisible = insets.isVisible(WindowInsetsCompat.Type.ime()); + WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); + Insets currentInsets = insets.getInsets( + WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); if (hasViewportCover) { int topInset = stableInsets.top; @@ -208,6 +214,7 @@ private WindowInsetsCompat applyInsets(View v, WindowInsetsCompat insets) { injectSafeAreaCSSWithBottom(topInset, currentInsets.right, bottomInset, currentInsets.left); } + // Branch for Legacy/Broken WebViews: Uses setViewMargins and CONSUMED to avoid double space if (hasBrokenWebViewVersion && hasViewportCover && v.hasWindowFocus() && v.isShown()) { if (keyboardVisible) { Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime()); @@ -220,6 +227,15 @@ private WindowInsetsCompat applyInsets(View v, WindowInsetsCompat insets) { resetViewBottomMargin(v); + // Workaround for Chromium bug #457682720 on modern WebViews + if (hasViewportCover && webViewVersion < WEBVIEW_VERSION_WITH_SAFE_AREA_KEYBOARD_FIX && keyboardVisible) { + return new WindowInsetsCompat.Builder(insets) + .setInsets( + WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), + Insets.of(currentInsets.left, currentInsets.top, currentInsets.right, 0) + ).build(); + } + return insets; } @@ -253,19 +269,19 @@ private void injectSafeAreaCSSWithBottom(int top, int right, int bottom, int lef getBridge().executeOnMainThread(() -> { if (bridge != null && bridge.getWebView() != null) { String script = String.format( - Locale.US, - """ - try { - document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx"); - } catch(e) { console.error('Error injecting safe area CSS:', e); } - """, - (int) topPx, - (int) rightPx, - (int) bottomPx, - (int) leftPx + Locale.US, + """ + try { + document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx"); + } catch(e) { console.error('Error injecting safe area CSS:', e); } + """, + (int) topPx, + (int) rightPx, + (int) bottomPx, + (int) leftPx ); bridge.getWebView().evaluateJavascript(script, null); @@ -297,10 +313,10 @@ private void injectSafeAreaCSS(int top, int right, int bottom, int left) { Locale.US, """ try { - document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx"); - document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-top", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-right", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-bottom", "%dpx"); + document.documentElement.style.setProperty("--safe-area-inset-left", "%dpx"); } catch(e) { console.error('Error injecting safe area CSS:', e); } """, (int) topPx, @@ -389,4 +405,4 @@ private int getWebViewMajorVersion() { cachedWebViewMajorVersion = 0; return 0; } -} +} \ No newline at end of file