Skip to content

Android: native-padding branch dispatches a live IME inset, so the WebView subtracts the keyboard twice (sibling of #107, other branch) #109

Description

@BuiltByMisha

Summary

On Android, on the native-padding branch (shouldPassthroughInsets == false), the soft keyboard is subtracted twice from the web viewport. setupSafeAreaInsets() correctly removes the input-method-editor (IME) inset as decor-view padding, but then returns WindowInsetsCompat with only systemBars() | displayCutout() zeroed — Type.ime() is left live. The System WebView installs its own inset listener (Chromium ≥ 140 handles safe-area and IME insets natively, which is what WEBVIEW_VERSION_WITH_SAFE_AREA_CORE_FIX exists for) and removes the same IME inset a second time.

This is the sibling of #107, on the other branch and by a different mechanism: #107 is about an ungated v.setPadding(...), this is about what the listener dispatches onward. #107 was closed as stale, so this branch's version has never been looked at.

Affected code

android/src/main/java/com/getcapacitor/community/safearea/SafeAreaPlugin.java, in setupSafeAreaInsets(), the shouldPassthroughInsets == false path (~line 137 in 8.0.1):

// We need to correct for a possible shown IME
v.setPadding(systemBarsInsets.left, systemBarsInsets.top, systemBarsInsets.right,
             keyboardVisible ? imeInsets.bottom : systemBarsInsets.bottom);   // <-- applied once, correctly

// Returning `WindowInsetsCompat.CONSUMED` breaks recalculation of safe area insets
// So we have to explicitly set insets to `0`
// See: https://issues.chromium.org/issues/461332423
return new WindowInsetsCompat.Builder(windowInsets).setInsets(
        WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),   // <-- ime() missing
        Insets.of(0, 0, 0, 0)
).build();

The comment above that setInsets already states the rule the fix follows: zero what you have applied, so nothing downstream re-applies it. Type.ime() was applied by the setPadding immediately above and is not in the mask.

Reproduction / measurement

Samsung SM-S931B (Galaxy S25), Android 16, 1080×2340, density 480, System WebView 150.0.7871.181, Capacitor 8.5.0, targetSdkVersion 36, EdgeToEdge.enable(), SystemBars.insetsHandling: "disable", and SafeArea config { initialViewportFitCover: false, detectViewportFitCoverChanges: false } — i.e. the native-padding branch forced on, deliberately, because most of the app's screens never consumed env(safe-area-inset-*) in CSS.

Keyboard closed: window.innerHeight 697, visualViewport.height 697.7.

Keyboard open, unmodified 8.0.1: innerHeight 61, visualViewport.height 61.7.

Keyboard open, with the fix below: innerHeight 403, visualViewport.height 403.7 — i.e. 1211 / 3.0, the single correct shrink.

An instrumented listener walking the real view chain, keyboard open, before the fix:

WEBVIEW-RECEIVES bars=Insets{...bottom=0} ime=Insets{...bottom=1026} imeVisible=true
DecorView              h=2340 padT=103 padB=1026 fitsSW=false
LinearLayout           h=1211 padB=0 fitsSW=true
FrameLayout            h=1211 padB=0
FitWindowsLinearLayout id=action_bar_root h=1211 padB=0 fitsSW=true
ContentFrameLayout     id=content h=1211 padB=0
CoordinatorLayout      h=1211 padB=0
CapacitorWebView       id=webview h=1211 padB=0

Two notes for anyone reproducing this, both of which cost time here:

  • uiautomator dump cannot answer this. It reports the android.webkit.WebView node at 185px, which is Chromium's own accessibility root reporting its shrunken viewport, not the View box. The View really is 1211px. The collapse is inside the WebView.
  • No native ancestor is involved. action_bar_root carries fitsSystemWindows="true" — the obvious suspect — and applies zero padding, precisely because the plugin already zeroed the inset types legacy fitSystemWindows consumes. Leaving ime() live is what reaches the WebView.

Invariant across interactive-widget resizes-content / overlays-content / absent, and independent of android:windowSoftInputMode, for the same reason #107 gives: the second subtraction lives in this native inset listener, not in any of those knobs.

Proposed fix

             return new WindowInsetsCompat.Builder(windowInsets).setInsets(
-                    WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout(),
+                    WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout() | WindowInsetsCompat.Type.ime(),
                     Insets.of(0, 0, 0, 0)
             ).build();

Verified on device via patch-package. Two independent interventions reach the same corrected value, which is what makes the attribution solid: clobbering the WebView's own inset listener from the host activity also gives innerHeight 403, but only by replacing Chromium's listener wholesale — correcting the dispatch is the narrow fix and leaves the WebView's own inset handling intact.

Note the passthrough branch has the same defect by the same mechanism (it also leaves ime() live in what it returns, on top of #107's ungated setPadding). I have not proposed a diff for it because this app never takes that branch and I could not measure it, but it looks like the same one-token change plus whatever #107 concluded.

Happy to open a pull request with just the diff above if that is easier to take than a report.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions