From e6a3128da36070acb1a93297e01eeab79a3e3ebb Mon Sep 17 00:00:00 2001 From: Storm Date: Sat, 2 May 2026 19:13:17 +0700 Subject: [PATCH 1/4] Enhance SystemBars with navigation bar visibility Added navigation bar visibility handling and adjusted safe area calculations. Updated insets handling for improved layout compatibility. --- .../com/getcapacitor/plugin/SystemBars.java | 79 ++++++++++++++----- 1 file changed, 58 insertions(+), 21 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 b335b4b1c..a4cab4144 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -59,6 +59,8 @@ function capacitorSystemBarsCheckMetaViewport() { private String currentStatusBarStyle = STYLE_DEFAULT; private String currentGestureBarStyle = STYLE_DEFAULT; + private boolean navBarVisible = true; + @Override public void load() { getBridge().getWebView().addJavascriptInterface(this, "CapacitorSystemBarsAndroidInterface"); @@ -76,7 +78,15 @@ protected void handleOnStart() { @Override public void onPageCommitVisible(WebView view, String url) { super.onPageCommitVisible(view, url); - getBridge().getWebView().requestApplyInsets(); + View parentView = (View) getBridge().getWebView().getParent(); + ViewCompat.requestApplyInsets(parentView); + if (insetHandlingEnabled) { + WindowInsetsCompat rootInsets = ViewCompat.getRootWindowInsets(parentView); + if (rootInsets != null) { + Insets safeArea = calcSafeAreaInsets(rootInsets); + injectSafeAreaCSS(safeArea.top, safeArea.right, safeArea.bottom, safeArea.left); + } + } } } ); @@ -103,8 +113,17 @@ private void initSystemBars() { initSafeAreaCSSVariables(); getBridge().executeOnMainThread(() -> { + Window window = getActivity().getWindow(); + WindowCompat.setDecorFitsSystemWindows(window, false); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { + window.setNavigationBarColor(android.graphics.Color.TRANSPARENT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.setNavigationBarContrastEnforced(false); + } + } setStyle(style, ""); setHidden(hidden, ""); + ViewCompat.requestApplyInsets((View) getBridge().getWebView().getParent()); }); } @@ -149,22 +168,41 @@ public void onDOMReady() { getActivity().runOnUiThread(() -> { this.bridge.getWebView().evaluateJavascript(viewportMetaJSFunction, (res) -> { hasViewportCover = res.equals("true"); - - getBridge().getWebView().requestApplyInsets(); + ViewCompat.requestApplyInsets((View) getBridge().getWebView().getParent()); }); }); } private Insets calcSafeAreaInsets(WindowInsetsCompat insets) { Insets safeArea = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); - if (insets.isVisible(WindowInsetsCompat.Type.ime())) { + + int bottom = safeArea.bottom; + + if (bottom == 0 + && Build.VERSION.SDK_INT < Build.VERSION_CODES.R + && safeArea.left == 0 && safeArea.right == 0) { // skip if nav bar is on a side (landscape) + bottom = getNavBarHeightFromResources(); + } + + boolean imeVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? insets.isVisible(WindowInsetsCompat.Type.ime()) + : insets.getInsets(WindowInsetsCompat.Type.ime()).bottom > 0; + + if (imeVisible) { return Insets.of(safeArea.left, safeArea.top, safeArea.right, 0); } - return Insets.of(safeArea.left, safeArea.top, safeArea.right, safeArea.bottom); + return Insets.of(safeArea.left, safeArea.top, safeArea.right, bottom); + } + + private int getNavBarHeightFromResources() { + if (!navBarVisible) return 0; + android.content.res.Resources res = getActivity().getResources(); + int heightId = res.getIdentifier("navigation_bar_height", "dimen", "android"); + return heightId > 0 ? res.getDimensionPixelSize(heightId) : 0; } private void initSafeAreaCSSVariables() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && insetHandlingEnabled) { + if (insetHandlingEnabled) { View v = (View) this.getBridge().getWebView().getParent(); WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(v); if (insets != null) { @@ -176,6 +214,10 @@ private void initSafeAreaCSSVariables() { private void initWindowInsetsListener() { ViewCompat.setOnApplyWindowInsetsListener((View) getBridge().getWebView().getParent(), (v, insets) -> { + // getRootWindowInsets() bypasses AppCompat's intermediate view consuming the bottom inset on API < 30 + WindowInsetsCompat rawInsets = ViewCompat.getRootWindowInsets(v); + WindowInsetsCompat safeAreaSource = (rawInsets != null) ? rawInsets : insets; + boolean shouldPassthroughInsets = getWebViewMajorVersion() >= WEBVIEW_VERSION_WITH_SAFE_AREA_FIX && hasViewportCover; Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()); @@ -186,8 +228,8 @@ private void initWindowInsetsListener() { // We need to correct for a possible shown IME v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && hasViewportCover && insetHandlingEnabled) { - Insets safeAreaInsets = calcSafeAreaInsets(insets); + if (hasViewportCover && insetHandlingEnabled) { + Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource); injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); } @@ -204,15 +246,7 @@ private void initWindowInsetsListener() { .build(); } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - // We need to correct for a possible shown IME - v.setPadding( - systemBarsInsets.left, - systemBarsInsets.top, - systemBarsInsets.right, - keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom - ); - } + v.setPadding(0, 0, 0, keyboardVisible ? imeInsets.bottom : 0); // Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets // So we have to explicitly set insets to `0` @@ -221,8 +255,8 @@ private void initWindowInsetsListener() { .setInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(), Insets.of(0, 0, 0, 0)) .build(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && hasViewportCover && insetHandlingEnabled) { - Insets safeAreaInsets = calcSafeAreaInsets(newInsets); + if (insetHandlingEnabled) { + Insets safeAreaInsets = calcSafeAreaInsets(safeAreaSource); injectSafeAreaCSS(safeAreaInsets.top, safeAreaInsets.right, safeAreaInsets.bottom, safeAreaInsets.left); } @@ -263,6 +297,7 @@ private void injectSafeAreaCSS(int top, int right, int bottom, int left) { } private void setStyle(String style, String bar) { + String requestedStyle = style; if (style.equals(STYLE_DEFAULT)) { style = getStyleForTheme(); } @@ -270,12 +305,12 @@ private void setStyle(String style, String bar) { Window window = getActivity().getWindow(); WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, window.getDecorView()); if (bar.isEmpty() || bar.equals(BAR_STATUS_BAR)) { - currentStatusBarStyle = style; + currentStatusBarStyle = requestedStyle; windowInsetsControllerCompat.setAppearanceLightStatusBars(!style.equals(STYLE_DARK)); } if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) { - currentGestureBarStyle = style; + currentGestureBarStyle = requestedStyle; windowInsetsControllerCompat.setAppearanceLightNavigationBars(!style.equals(STYLE_DARK)); } @@ -292,6 +327,7 @@ private void setHidden(boolean hide, String bar) { } if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) { windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.navigationBars()); + navBarVisible = false; } return; } @@ -301,6 +337,7 @@ private void setHidden(boolean hide, String bar) { } if (bar.isEmpty() || bar.equals(BAR_GESTURE_BAR)) { windowInsetsControllerCompat.show(WindowInsetsCompat.Type.navigationBars()); + navBarVisible = true; } } From 37bb5c909c60c3e80f11c40e54c9494b6898113b Mon Sep 17 00:00:00 2001 From: Storm Date: Wed, 6 May 2026 07:13:07 +0000 Subject: [PATCH 2/4] fix(SystemBars): add handleOnResume to manage navigation bar appearance --- .../com/getcapacitor/plugin/SystemBars.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 a4cab4144..c77efda72 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -163,6 +163,24 @@ public void setAnimation(final PluginCall call) { call.resolve(); } + @Override + protected void handleOnResume() { + super.handleOnResume(); + getBridge().executeOnMainThread(() -> { + Window window = getActivity().getWindow(); + WindowCompat.setDecorFitsSystemWindows(window, false); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { + window.setNavigationBarColor(android.graphics.Color.TRANSPARENT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.setNavigationBarContrastEnforced(false); + } + } + setStyle(currentGestureBarStyle, BAR_GESTURE_BAR); + setStyle(currentStatusBarStyle, BAR_STATUS_BAR); + ViewCompat.requestApplyInsets((View) getBridge().getWebView().getParent()); + }); + } + @JavascriptInterface public void onDOMReady() { getActivity().runOnUiThread(() -> { From 6d4285ae9799bca080209af1e0ab389af0a68016 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 02:29:10 +0000 Subject: [PATCH 3/4] fix(android): silence InternalInsetResource lint on nav bar height fallback Co-authored-by: Martin DONADIEU --- .../src/main/java/com/getcapacitor/plugin/SystemBars.java | 5 +++++ 1 file changed, 5 insertions(+) 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 fef4bc085..820390f3d 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.annotation.SuppressLint; import android.content.Context; import android.content.pm.PackageInfo; import android.content.res.Configuration; @@ -211,6 +212,10 @@ private Insets calcSafeAreaInsets(WindowInsetsCompat insets) { return Insets.of(safeArea.left, safeArea.top, safeArea.right, bottom); } + // Only reached on API < 30 when the window reports no inset on any edge, so the + // choice is this approximation or a hardcoded 0. Lint rejects the internal + // resource because OEMs may change it; a wrong height still beats no inset. + @SuppressLint("InternalInsetResource") private int getNavBarHeightFromResources() { if (!navBarVisible) return 0; android.content.res.Resources res = getActivity().getResources(); From 84c53dab01868e789b61b23fe4971a90aeaf0907 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 13 Aug 2026 02:40:31 +0000 Subject: [PATCH 4/4] fix(android): also suppress DiscouragedApi on nav bar height fallback Co-authored-by: Martin DONADIEU --- .../src/main/java/com/getcapacitor/plugin/SystemBars.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 820390f3d..9b1df78e0 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/SystemBars.java @@ -215,7 +215,7 @@ private Insets calcSafeAreaInsets(WindowInsetsCompat insets) { // Only reached on API < 30 when the window reports no inset on any edge, so the // choice is this approximation or a hardcoded 0. Lint rejects the internal // resource because OEMs may change it; a wrong height still beats no inset. - @SuppressLint("InternalInsetResource") + @SuppressLint({ "InternalInsetResource", "DiscouragedApi" }) private int getNavBarHeightFromResources() { if (!navBarVisible) return 0; android.content.res.Resources res = getActivity().getResources();