fix(embed): napplet/nsite load overlay + draw it over the surface

Mirrors the web-app load-recovery fix to the napplet/nsite path and fixes a
layer bug that kept the overlay from ever showing.

The embedded surface (SandboxedSdkView) is drawn by EmbeddedTabLayer, which
sits *above* the nav screens in the shell. So a loading/error overlay placed in
the favorite screen was covered by the surface's opaque pre-first-frame
background — the black void persisted. Move the overlay into EmbeddedTabLayer,
drawn over the active tab's bounds (where the chrome sheet already lives), so it
actually covers the surface. Also fixes the overlay sizing (fillMaxSize, not
matchParentSize, which collapsed to zero inside the reserved Box).

- Promote load state to the EmbeddedSurfaceController interface (loadStatus /
  onLoadStatusChanged / retry), so EmbeddedTabLayer renders one overlay for both
  the browser and napplet controllers. Shared EmbeddedLoadStatus +
  EmbeddedLoadOverlay.
- NappletHostService now reports main-frame load state (start/finish/error) over
  a new MSG_LOAD_STATE; EmbeddedNappletController relays it and exposes retry()
  (= reload the verified content).
- The web-app path keeps its about:blank → canonical-URL self-heal; the napplet
  path has no client-supplied URL to drop, so retry = reload.

Verified on device: with the network cut, the brainstorm tab shows
"Couldn't load this app." + Retry; restoring the network and tapping Retry loads
the page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-06-24 14:35:44 -04:00
co-authored by Claude Opus 4.8
parent fb4a2e0858
commit cad987a99e
10 changed files with 275 additions and 95 deletions
@@ -77,8 +77,17 @@ object NappletEmbedContract {
/** Client → provider: an IME editing op for the focused field; raw JSON in [KEY_IME_PAYLOAD]. */
const val MSG_IME_OP = 14
/**
* Provider → client: the main-frame load state changed. Carries [KEY_IS_LOADING] (a load is in
* flight) and [KEY_LOAD_FAILED] (the main frame errored). Lets the main process draw a loading
* spinner / error+retry overlay over the embedded surface instead of a bare black/white void.
*/
const val MSG_LOAD_STATE = 15
const val KEY_CORE_LIB_INFO = "coreLibInfo"
const val KEY_CAN_GO_BACK = "canGoBack"
const val KEY_IS_LOADING = "isLoading"
const val KEY_LOAD_FAILED = "loadFailed"
const val KEY_NOTICE = "notice"
const val KEY_IME_PAYLOAD = "imePayload"
@@ -35,6 +35,7 @@ import android.os.Message
import android.os.Messenger
import android.util.Log
import android.view.View
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings
@@ -99,6 +100,10 @@ class NappletHostService : Service() {
var webView: WebView? = null
var bridgeReplyProxy: JavaScriptReplyProxy? = null
var fireSeq = 0
// Last main-frame error state, pushed to the client so it can show an error/retry overlay over the
// surface (the embedded surface has no error page of its own).
var loadFailed = false
val replyMessenger = Messenger(Handler(Looper.getMainLooper()) { onBrokerReply(this, it) })
}
@@ -311,6 +316,16 @@ class NappletHostService : Service() {
request: WebResourceRequest,
): WebResourceResponse? = tab.contentServer?.serve(request)
override fun onPageStarted(
view: WebView,
url: String,
favicon: android.graphics.Bitmap?,
) {
// A new main-frame navigation cleared any prior error.
tab.loadFailed = false
pushLoadState(tab, isLoading = true)
}
override fun doUpdateVisitedHistory(
view: WebView,
url: String,
@@ -320,7 +335,22 @@ class NappletHostService : Service() {
override fun onPageFinished(
view: WebView,
url: String,
) = pushState(tab, view)
) {
pushState(tab, view)
pushLoadState(tab, isLoading = false)
}
override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError,
) {
// Only a main-frame failure blanks the applet; a missing sub-resource is irrelevant to whether
// it opened.
if (!request.isForMainFrame) return
tab.loadFailed = true
pushLoadState(tab, isLoading = false)
}
override fun shouldOverrideUrlLoading(
view: WebView,
@@ -346,6 +376,22 @@ class NappletHostService : Service() {
runCatching { tab.clientMessenger?.send(message) }
}
/** Tells the client whether a main-frame load is in flight and whether it failed, so it can overlay a spinner/retry. */
private fun pushLoadState(
tab: NappletTab,
isLoading: Boolean,
) {
val message =
Message.obtain(null, NappletEmbedContract.MSG_LOAD_STATE).apply {
data =
Bundle().apply {
putBoolean(NappletEmbedContract.KEY_IS_LOADING, isLoading)
putBoolean(NappletEmbedContract.KEY_LOAD_FAILED, tab.loadFailed)
}
}
runCatching { tab.clientMessenger?.send(message) }
}
// ---- bridge: shell <-> native (mirror of NappletHostActivity.onShellMessage) ----
private fun onShellMessage(