ionic-team / ionic-team/capacitor
[Bug]: SystemBars reports a zero bottom inset on API < 30 when the app uses EdgeToEdge.enable() (content renders under the navigation bar)
- Dominant language
- TypeScript
- Stars
- 16.7k
- Forks
- 1.3k
- Avg merge
- 3d 15h
- Merged PRs (30d)
- 10
Description
### Capacitor Version
```
@capacitor/android 8.5.2
@capacitor/core 8.5.2
@capacitor/keyboard 8.0.5
@capacitor/splash-screen 8.0.2
```
### Other API Details
```
Android: compileSdk/targetSdk 36, minSdk 26
Affected device: PAX A920Pro, Android 10 (API 29), Android System WebView 147.0.7727.137
Also reported by our users: Saturn1000F2, Android 9 (API 28), WebView 135
Not affected: same app on Android 11+ (API 30+), any WebView
The activity calls androidx `EdgeToEdge.enable(this)` in onCreate
Page declares
capacitor.config: "SystemBars": { "initialViewportFitValueHint": "cover" } (insetsHandling default "css")
```
### Platforms Affected
- [ ] iOS
- [x] Android
- [ ] Web
### Current Behavior
On **API < 30**, when the activity enables edge-to-edge with androidx `EdgeToEdge.enable()`, `SystemBars` reports a **bottom inset of 0** while the navigation bar is visible and drawn over the window. Web content therefore renders underneath the navigation bar. The top inset is unaffected.
Measured on a PAX A920Pro (Android 10, 720x1440 @ 320dpi, dpr 2), page read through remote debugging:
```
--safe-area-inset-top: 24px env(safe-area-inset-top): 24px
--safe-area-inset-bottom: 0px env(safe-area-inset-bottom): 0px
innerHeight: 720 (= full screen height, window is edge-to-edge)
```
while `dumpsys window windows` shows the navigation bar is present and 48dp tall:
```
Window{... NavigationBar0}: mFrame=[0,1344][720,1440] # 96px = 48dp
Window{... StatusBar}: mFrame=[0,0][720,48] # 48px = 24dp
Window{... MainActivity}: mFrame=[0,0][720,1440] # window covers both bars
vsysui=LAYOUT_STABLE LAYOUT_HIDE_NAVIGATION LAYOUT_FULLSCREEN LIGHT_STATUS_BAR LIGHT_NAVIGATION_BAR
mSystemUiVisibility=0x2710
```
Other apps on the same device get `mContentInsets=[0,48][0,96]`, so the navigation bar is a normal inset provider on this ROM.
The flags above are what `EdgeToEdge.enable()` sets on API < 30 (`SYSTEM_UI_FLAG_LAYOUT_STABLE | LAYOUT_HIDE_NAVIGATION | LAYOUT_FULLSCREEN`). `LAYOUT_HIDE_NAVIGATION` tells the platform the window lays itself out under the navigation bar, so the navigation bar is dropped from the window's system-window insets. `initWindowInsetsListener` reads exactly those:
```java
View view = getActivity().getWindow().getDecorView();
ViewCompat.setOnApplyWindowInsetsListener(view, (v, insets) -> {
Insets systemBarsInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
```
On API < 30 `WindowInsetsCompat.getInsets()` resolves to the system-window insets, which is where the zero comes from; on API 30+ it maps to the real `WindowInsets.getInsets(Type)` API, which the legacy layout flags do not affect — hence the version split.
Both branches of the listener are affected, so the symptom does not depend on the WebView version: with WebView >= 140 the zero is passed through to `env(safe-area-inset-bottom)`, and with WebView < 140 the decor view is padded by 0 and `--safe-area-inset-bottom: 0px` is injected.
We started seeing this after upgrading 8.4.2 -> 8.5.2. In 8.4.x the listener was attached to the WebView's parent rather than the decor view (changed in #8535), which we believe is why the app was not exposed to the window-level layout flags before.
### Expected Behavior
`SystemBars` reports the navigation bar inset whenever the navigation bar is visible, independent of the legacy `SYSTEM_UI_FLAG_LAYOUT_*` flags that `EdgeToEdge.enable()` sets on API < 30 — the same values the app gets on API 30+.
`WindowInsetsCompat.getInsetsIgnoringVisibility()` returns the correct value here: for `NAVIGATION_BARS` on API < 30 it resolves to the stable insets, which the layout flags do not zero. Guarding it with `isVisible()` keeps apps that genuinely hide the bars (immersive/fullscreen) unpadded:
```java
Insets systemBarsInsets = Build.VERSION.SDK_INT < Build.VERSION_CODES.R && insets.isVisible(WindowInsetsCompat.Type.navigationBars())
? insets.getInsetsIgnoringVisibility(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout())
: insets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
```
As a workaround we now call `EdgeToEdge.enable()` only on API 30+, which removes the flags and fixes the overlap, at the cost of edge-to-edge rendering (and therefore the system bar colours) on older devices.
### Additional Information
Possibly related to #8601, which is a different symptom in the same listener on API < 30.
Contributor guide
Research direction
Start at the Android SystemBars initWindowInsetsListener, especially the WindowInsetsCompat.Type.systemBars() handling on API < 30. Reproduce with EdgeToEdge.enable() on Android 9 or 10, then verify that visible navigation bars report their stable bottom inset while hidden bars remain unpadded.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, java
- Domain
- mobile-dev
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100