fix(napplet): launch the sandbox host by reading shell/shim from assets

NappletHostActivity runs in the isolated `:napplet` process, which early-returns
from Amethyst.onCreate to stay key-free and so never initializes the
compose-resources Android context. `Res.readBytes` then threw
MissingResourceException, crashing the host 100% on launch (the napplet feature
could never open).

Read shell.html/shim.js straight from the APK assets (where compose-resources
packages them) via the Activity context instead of the suspending Res accessor.
NappletWebContract now exposes the relative paths + RESOURCE_ASSET_ROOT so the
paths stay single-sourced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vitor Pamplona
2026-06-22 14:28:32 -04:00
co-authored by Claude Opus 4.8
parent a277f6a65f
commit 44deeb1ee0
2 changed files with 17 additions and 4 deletions
@@ -48,7 +48,6 @@ import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson
import com.vitorpamplona.amethyst.commons.napplet.resolveRequiredCapabilities
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
/**
@@ -130,7 +129,12 @@ class NappletHostActivity : ComponentActivity() {
// The shell page + shim are the shared web contract (commons composeResources); load them
// once up front so the WebView worker threads that serve them never block on resource I/O.
val (shellHtml, shim) = runBlocking { NappletWebContract.shellHtml() to NappletWebContract.shimJs().decodeToString() }
// This isolated `:napplet` process skips app init (to stay key-free), so the compose-resources
// Android context is never set here and `Res.readBytes` would crash — read the contract bytes
// straight from the APK assets, where compose-resources packages them.
fun readContractAsset(path: String): ByteArray = assets.open(NappletWebContract.RESOURCE_ASSET_ROOT + path).use { it.readBytes() }
val shellHtml = readContractAsset(NappletWebContract.SHELL_HTML_PATH)
val shim = readContractAsset(NappletWebContract.SHIM_JS_PATH).decodeToString()
contentServer = NappletContentServer(paths, servers, proxyPort, cacheDir, shellHtml, shim)
webView = WebView(this)
@@ -68,8 +68,17 @@ object NappletWebContract {
"media-src 'self' https://napplet.local blob: data:; " +
"connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'self'; form-action 'none'"
private const val SHELL_HTML_PATH = "files/napplet/shell.html"
private const val SHIM_JS_PATH = "files/napplet/shim.js"
const val SHELL_HTML_PATH = "files/napplet/shell.html"
const val SHIM_JS_PATH = "files/napplet/shim.js"
/**
* Asset directory the compose-resources of `:commons` are packaged under on Android (mirrors the
* prefix the generated [Res] accessor prepends). Hosts that run in a process without an
* initialized compose-resources context — e.g. the isolated `:napplet` process, which skips app
* init to stay key-free — read [SHELL_HTML_PATH]/[SHIM_JS_PATH] straight from `assets` under this
* root instead of going through [shellHtml]/[shimJs].
*/
const val RESOURCE_ASSET_ROOT = "composeResources/com.vitorpamplona.amethyst.commons.resources/"
/** The trusted shell page's HTML, read from the shared bundle. */
@OptIn(ExperimentalResourceApi::class)