Merge pull request #3932 from vitorpamplona/claude/ime-padding-browser-window-ksa26a

Handle IME insets in full-screen napplet hosts
This commit is contained in:
Vitor Pamplona
2026-08-14 20:54:50 -04:00
committed by GitHub
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.
*