From 582b7dfe2e1ce7b8d1bc1a6da638b9deaee6d599 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 00:51:25 +0000 Subject: [PATCH] fix: inset the full-screen browser and napplet host for the IME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit windowSoftInputMode=adjustResize no longer resizes the window: it is a no-op for apps targeting SDK 35+ on Android 15+, where edge-to-edge is enforced and Theme.Amethyst does not opt out. The full-screen browser padded its root by the system bars and display cutout only, relying on that resize to keep focused inputs visible, so the soft keyboard simply covered the bottom of the page. Pad the root by the IME inset as well — max(bars, ime) on the bottom, since an open IME sits over the navigation bar — and zero the consumed types before they reach the WebView, which would otherwise apply them to its own web content a second time. Shared as applyFullScreenHostInsets between NappletBrowserActivity and NappletHostActivity, which carried the same listener and the same defect. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BwZuom4fFXjr85aGajJhSo --- amethyst/src/main/AndroidManifest.xml | 4 ++- .../napplethost/NappletBrowserActivity.kt | 23 +++++-------- .../napplethost/NappletHostActivity.kt | 20 +++--------- .../napplethost/NappletWebViewInsets.kt | 32 +++++++++++++++++++ 4 files changed, 47 insertions(+), 32 deletions(-) diff --git a/amethyst/src/main/AndroidManifest.xml b/amethyst/src/main/AndroidManifest.xml index 0a15d222cd..2435d9430b 100644 --- a/amethyst/src/main/AndroidManifest.xml +++ b/amethyst/src/main/AndroidManifest.xml @@ -559,7 +559,9 @@ + natively. The activity insets its own content for the soft keyboard (adjustResize only still + applies below Android 15, where the window is not forced edge-to-edge). Its own task/recents + entry. --> - val applied = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() - val bars = insets.getInsets(applied) - view.setPadding(bars.left, bars.top, bars.right, bars.bottom) - WindowInsetsCompat.Builder(insets).setInsets(applied, Insets.NONE).build() - } + // Pad by the system bars + cutout AND the IME: on an edge-to-edge window (enforced for targetSdk + // 35+ on Android 15+) windowSoftInputMode=adjustResize no longer shrinks the window, so without + // this the keyboard covers the bottom of the page. See applyFullScreenHostInsets. + root.applyFullScreenHostInsets() contentFrame.addView(webView, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)) loadingView = buildLoadingView().also { contentFrame.addView(it) } diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt index 74c3c6aaac..85f0803cf2 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt @@ -56,9 +56,6 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.OnBackPressedCallback import androidx.core.content.ContextCompat -import androidx.core.graphics.Insets -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat import androidx.webkit.JavaScriptReplyProxy import androidx.webkit.ProxyConfig import androidx.webkit.ProxyController @@ -304,19 +301,10 @@ class NappletHostActivity : ComponentActivity() { addView(topProgressBar) } setContentView(root) - // Activities are edge-to-edge by default on recent Android; pad by the system bar and - // display-cutout insets so neither the chrome nor the applet draws under the system bars. - ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets -> - val applied = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() - val bars = insets.getInsets(applied) - view.setPadding(bars.left, bars.top, bars.right, bars.bottom) - // Zero the insets we just turned into padding before they reach the child WebView: on - // targetSdk 35+ the WebView auto-applies any insets it receives to its own web content, so - // leaving them un-consumed padded the bottom a SECOND time — a band of the page's own - // background above the navigation bar. Keep the other types (notably IME) flowing so the - // applet's keyboard-aware resize still works. - WindowInsetsCompat.Builder(insets).setInsets(applied, Insets.NONE).build() - } + // Activities are edge-to-edge by default on recent Android; pad by the system bar, display-cutout + // and IME insets so neither the chrome nor the applet draws under the system bars or the soft + // keyboard (adjustResize no longer resizes an edge-to-edge window). See applyFullScreenHostInsets. + root.applyFullScreenHostInsets() probeAndMount() } diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletWebViewInsets.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletWebViewInsets.kt index f24531c91d..74374cd8be 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletWebViewInsets.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletWebViewInsets.kt @@ -20,11 +20,43 @@ */ package com.vitorpamplona.amethyst.napplethost +import android.view.View import android.webkit.WebView import androidx.core.graphics.Insets import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +/** + * Insets the root of a **full-screen** sandbox host (browser / napplet / nsite) by the system bars, + * the display cutout, **and the IME**, then hides those types from its children. + * + * The IME half is not optional on current Android. These activities used to lean on + * `windowSoftInputMode="adjustResize"` to shrink the window when the keyboard opened — the WebView + * shrank with it and the focused input stayed visible. That flag is a **no-op for apps targeting SDK + * 35+** running on Android 15+: edge-to-edge is enforced, the window keeps its full height, and the + * keyboard simply draws over the bottom of the page. Padding the root by the IME inset reproduces the + * old resize behaviour on our side of the window. + * + * The bottom is `max(bars, ime)` rather than a sum because an open IME sits *over* the navigation bar; + * adding them would leave a dead band the height of the nav bar above the keyboard. + * + * Zeroing the consumed types before they reach the children matters for the same reason + * [dropSystemBarInsets] exists: on targetSdk 35+ a WebView applies whatever insets it receives to its + * own web content, so leaving them un-consumed pads the page a second time. + */ +fun View.applyFullScreenHostInsets() { + ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> + val barTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + val bars = insets.getInsets(barTypes) + val ime = insets.getInsets(WindowInsetsCompat.Type.ime()) + view.setPadding(bars.left, bars.top, bars.right, maxOf(bars.bottom, ime.bottom)) + WindowInsetsCompat + .Builder(insets) + .setInsets(barTypes or WindowInsetsCompat.Type.ime(), Insets.NONE) + .build() + } +} + /** * Stops an **embedded** sandbox WebView from inseting its own web content for the system bars. *