diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt index a14a77b7c3..199b8d7961 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletBrowserActivity.kt @@ -35,6 +35,7 @@ import android.os.Messenger import android.util.Log import android.view.Gravity import android.view.View +import android.view.ViewGroup import android.webkit.ConsoleMessage import android.webkit.WebChromeClient import android.webkit.WebResourceError @@ -231,7 +232,6 @@ class NappletBrowserActivity : ComponentActivity() { super.onResume() if (this::webView.isInitialized) { webView.onResume() - webView.resumeTimers() } resumed = true heartbeatHandler.removeCallbacks(heartbeat) @@ -240,8 +240,11 @@ class NappletBrowserActivity : ComponentActivity() { override fun onPause() { if (this::webView.isInitialized) { + // Only pause THIS activity's WebView (onPause is per-WebView). Do NOT call pauseTimers(): it is + // process-global — it freezes JS/layout/parsing timers for EVERY WebView in `:napplet`, including + // the embedded ones in NappletBrowserService, which have no resume of their own. That left the + // embed frozen (dead page/connection) after returning from a full-screen excursion. webView.onPause() - webView.pauseTimers() } resumed = false heartbeatHandler.removeCallbacks(heartbeat) @@ -251,7 +254,17 @@ class NappletBrowserActivity : ComponentActivity() { override fun onDestroy() { runCatching { unbindService(brokerConnection) } - if (this::webView.isInitialized) webView.destroy() + if (this::webView.isInitialized) { + // Detach from the view tree BEFORE destroy(). Destroying a WebView while it is still attached to + // the window corrupts the SHARED multiprocess renderer/network state, which then breaks the OTHER + // (embedded) WebViews living in this `:napplet` process: dead DNS (ERR_NAME_NOT_RESOLVED), DOM reads + // returning empty (`value == ""` on a field that visibly shows text), dead selection-highlight paint, + // and broken IME — all after a full-screen excursion returns to an embed. (`destroy()` requires the + // view to be removed from the hierarchy first; see WebView.destroy() docs.) + webView.stopLoading() + (webView.parent as? ViewGroup)?.removeView(webView) + webView.destroy() + } super.onDestroy() } 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 82f71a4c19..771ff3c470 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostActivity.kt @@ -321,7 +321,6 @@ class NappletHostActivity : ComponentActivity() { super.onResume() if (this::webView.isInitialized) { webView.onResume() - webView.resumeTimers() } // Launching this :napplet-process surface backgrounded the main process; tell the broker to // hold the main process resumed (Tor/relays/AUTH) while this napplet/nSite is in front, and @@ -335,8 +334,10 @@ class NappletHostActivity : ComponentActivity() { // sign/decrypt/pay request whose consent prompt would surface over (and be confused with) // Amethyst's own UI. Requests only happen while the user is looking at this napplet. if (this::webView.isInitialized) { + // webView.onPause() pauses THIS WebView's JS/DOM (the security goal — a backgrounded napplet can't + // fire a sign/decrypt/pay request). Do NOT call pauseTimers(): it's process-global and freezes + // EVERY WebView in `:napplet`, including the embedded browser/napplet surfaces, which never resume. webView.onPause() - webView.pauseTimers() } // No longer foreground: stop renewing and let the main process resume normal background scaling. resumed = false @@ -384,6 +385,11 @@ class NappletHostActivity : ComponentActivity() { runCatching { unbindService(brokerConnection) } keyActions.clear() if (this::webView.isInitialized) { + // Detach before destroy(): destroying an attached WebView corrupts the shared multiprocess + // renderer/network state and breaks the other (embedded) WebViews in this `:napplet` process + // (dead DNS, empty DOM reads, dead selection paint, broken IME). See NappletBrowserActivity. + webView.stopLoading() + (webView.parent as? ViewGroup)?.removeView(webView) webView.destroy() } super.onDestroy() diff --git a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt index 1e57f3af1e..edde38c068 100644 --- a/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt +++ b/nappletHost/src/main/kotlin/com/vitorpamplona/amethyst/napplethost/NappletHostService.kt @@ -172,16 +172,11 @@ class NappletHostService : Service() { } NappletEmbedContract.MSG_BACK -> tabFor(msg)?.webView?.let { if (it.canGoBack()) it.goBack() } NappletEmbedContract.MSG_RELOAD -> tabFor(msg)?.webView?.reload() - NappletEmbedContract.MSG_PAUSE -> - tabFor(msg)?.webView?.let { - it.onPause() - it.pauseTimers() - } - NappletEmbedContract.MSG_RESUME -> - tabFor(msg)?.webView?.let { - it.onResume() - it.resumeTimers() - } + // onPause()/onResume() are per-WebView (pause/resume THIS surface's JS/DOM). Do NOT call + // pauseTimers()/resumeTimers(): they are process-global and would freeze/thaw every WebView in + // `:napplet` (the browser embed + other napplets), whose lifecycles are independent of this one. + NappletEmbedContract.MSG_PAUSE -> tabFor(msg)?.webView?.onPause() + NappletEmbedContract.MSG_RESUME -> tabFor(msg)?.webView?.onResume() NappletEmbedContract.MSG_IME_OP -> { val tab = tabFor(msg) ?: return true val payload = msg.data?.getString(NappletEmbedContract.KEY_IME_PAYLOAD) ?: return true