fix: inset the full-screen browser and napplet host for the IME

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwZuom4fFXjr85aGajJhSo
This commit is contained in:
Claude
2026-08-15 00:51:25 +00:00
parent 9eba7b9296
commit 582b7dfe2e
4 changed files with 47 additions and 32 deletions
+3 -1
View File
@@ -559,7 +559,9 @@
<!-- Direct-WebView browser for a single web client. Runs in the isolated, keyless `:napplet`
process and hosts the WebView directly (not a streamed surface), so scroll/zoom/keyboard work
natively. adjustResize shrinks the window for the soft keyboard. Its own task/recents entry. -->
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. -->
<activity
android:name="com.vitorpamplona.amethyst.napplethost.NappletBrowserActivity"
android:process=":napplet"
@@ -53,11 +53,8 @@ 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.graphics.scale
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.webkit.JavaScriptReplyProxy
import androidx.webkit.ProxyConfig
import androidx.webkit.ProxyController
@@ -76,9 +73,10 @@ import com.vitorpamplona.amethyst.commons.R as CommonsR
* process. Unlike the embedded browser ([NappletBrowserService], which streams its surface to the main
* app through SurfaceControlViewHost — a path that, on current Android, forwards taps but drops scroll/
* zoom/keyboard gestures), this hosts the WebView **directly** in its own window, so scrolling, pinch
* zoom, and the soft keyboard (`adjustResize`) all work natively. It stays just as keyless: the page JS
* runs here, every NIP-07 `window.nostr` call is brokered + consent-gated in the main process per origin,
* and the keys never leave it.
* zoom, and the soft keyboard all work natively — the window insets the content for the IME itself (see
* [applyFullScreenHostInsets]). It stays just as keyless: the page JS runs here, every NIP-07
* `window.nostr` call is brokered + consent-gated in the main process per origin, and the keys never
* leave it.
*
* Mirrors [NappletHostActivity]'s sandbox scaffolding (trusted chrome, loading screen, foreground hold)
* but loads a live URL directly instead of serving verified blobs through a shell.
@@ -214,15 +212,10 @@ class NappletBrowserActivity : ComponentActivity() {
addView(topProgressBar)
}
setContentView(root)
// Pad by the system bars + cutout, but NOT the IME — windowSoftInputMode=adjustResize shrinks the
// window when the keyboard shows, and the WebView (filling contentFrame) resizes so focused inputs
// stay visible. Zero the consumed insets before they reach the WebView so it doesn't double-pad.
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)
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) }
@@ -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()
}
@@ -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.
*