From 44deeb1ee0303df49238695e5439f0763ef04b2d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 22 Jun 2026 14:28:32 -0400 Subject: [PATCH] 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) --- .../amethyst/napplet/NappletHostActivity.kt | 8 ++++++-- .../amethyst/commons/napplet/NappletWebContract.kt | 13 +++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletHostActivity.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletHostActivity.kt index 118a8ede24..9e641973a3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletHostActivity.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/napplet/NappletHostActivity.kt @@ -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) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletWebContract.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletWebContract.kt index cde595faf1..7b85ccdaea 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletWebContract.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/napplet/NappletWebContract.kt @@ -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)