mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 08:27:04 +00:00
refactor: session-scope NappletHostService so multiple napplet/nsite tabs are correct
Mirror the browser host's per-session refactor for the napplet/nsite embed provider. A single NappletHostService instance is shared by every embedded tab (same bound Intent), but it kept one set of fields (client messenger, config, content server, WebView, broker bridge), so with >1 napplet tab open the controls (reload/back/pause/resume), navigation state, "allow always" notices, NIP-07 traffic, and IME all routed to whichever tab was created last. Collect all per-surface state into a NappletTab keyed by a client-stamped session id (KEY_SESSION_ID on MSG_CREATE_SESSION and every control message): - Controls/pause/resume resolve the target tab by id and act on its own WebView. - Content server, shell handshake (declaredDomains), launch token, and page state/notices are per tab; onShellMessage resolves the tab by its WebView. - Each tab gets its OWN reply Messenger, so broker responses AND unsolicited relay pushes come back tagged to the right tab — no id rewriting, per-tab origin/fire-seq state. - onSessionClosed drops the tab and destroys only its own WebView; the broker is bound once for the whole service. EmbeddedNappletController generates a unique id and stamps it on all messages. Both embed hosts (browser + napplet) are now fully per-tab correct, including the keyboard for multiple simultaneous tabs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MgMpRcWj6y82LxLiwcuzmN
This commit is contained in:
+23
-7
@@ -41,6 +41,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedImeBridge
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedSurfaceController
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.ImeEvent
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
/**
|
||||
* Client-side handle to an embedded nsite/napplet. Binds [NappletHostService][com.vitorpamplona.amethyst.napplethost.NappletHostService]
|
||||
@@ -65,6 +66,10 @@ class EmbeddedNappletController(
|
||||
private var sandboxedSdkView: SandboxedSdkView? = null
|
||||
private var pendingAdapter: SandboxedUiAdapter? = null
|
||||
|
||||
// A single NappletHostService instance serves every embedded napplet tab, so each controller stamps
|
||||
// its own id on every message; the provider uses it to route controls/state/IME to this tab.
|
||||
private val sessionId: String = "napplet-${SESSION_SEQ.incrementAndGet()}"
|
||||
|
||||
// A parked tab can be hidden (paused) before the service even binds, so the pause message is
|
||||
// dropped (no messenger yet). Remember the intent and replay it right after the session is created,
|
||||
// otherwise an applet that was never shown comes up running in the background.
|
||||
@@ -133,7 +138,7 @@ class EmbeddedNappletController(
|
||||
val msg =
|
||||
Message.obtain(null, NappletEmbedContract.MSG_CREATE_SESSION).apply {
|
||||
replyTo = incoming
|
||||
data = Bundle(params)
|
||||
data = Bundle(params).apply { putString(NappletEmbedContract.KEY_SESSION_ID, sessionId) }
|
||||
}
|
||||
runCatching { serviceMessenger?.send(msg) }
|
||||
// Replay a pause that was requested before we had a messenger to send it on (parked-before-bound),
|
||||
@@ -167,10 +172,7 @@ class EmbeddedNappletController(
|
||||
return true
|
||||
}
|
||||
|
||||
override fun sendImeOp(json: String) {
|
||||
val msg = Message.obtain(null, NappletEmbedContract.MSG_IME_OP).apply { data = Bundle().apply { putString(NappletEmbedContract.KEY_IME_PAYLOAD, json) } }
|
||||
runCatching { serviceMessenger?.send(msg) }
|
||||
}
|
||||
override fun sendImeOp(json: String) = send(NappletEmbedContract.MSG_IME_OP) { putString(NappletEmbedContract.KEY_IME_PAYLOAD, json) }
|
||||
|
||||
private fun parseImeEvent(payload: String): ImeEvent? {
|
||||
val o = runCatching { JSONObject(payload) }.getOrNull() ?: return null
|
||||
@@ -210,8 +212,22 @@ class EmbeddedNappletController(
|
||||
send(NappletEmbedContract.MSG_RESUME)
|
||||
}
|
||||
|
||||
private fun send(what: Int) {
|
||||
val msg = Message.obtain(null, what)
|
||||
private inline fun send(
|
||||
what: Int,
|
||||
crossinline block: Bundle.() -> Unit = {},
|
||||
) {
|
||||
val msg =
|
||||
Message.obtain(null, what).apply {
|
||||
data =
|
||||
Bundle().apply {
|
||||
putString(NappletEmbedContract.KEY_SESSION_ID, sessionId)
|
||||
block()
|
||||
}
|
||||
}
|
||||
runCatching { serviceMessenger?.send(msg) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private val SESSION_SEQ = AtomicLong()
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -82,6 +82,13 @@ object NappletEmbedContract {
|
||||
const val KEY_NOTICE = "notice"
|
||||
const val KEY_IME_PAYLOAD = "imePayload"
|
||||
|
||||
/**
|
||||
* Opaque per-tab session id the client stamps on [MSG_CREATE_SESSION] and every control message. A
|
||||
* single provider instance is shared by all embedded napplet/nSite tabs (they bind the same Intent),
|
||||
* so this scopes a control to the right surface and routes state/notices/IME back to the right tab.
|
||||
*/
|
||||
const val KEY_SESSION_ID = "sessionId"
|
||||
|
||||
const val NOTICE_PUBLISHED = "published"
|
||||
const val NOTICE_UPLOADED = "uploaded"
|
||||
const val NOTICE_PAID = "paid"
|
||||
|
||||
+132
-88
@@ -72,29 +72,40 @@ import java.util.concurrent.Executor
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
class NappletHostService : Service() {
|
||||
private val incoming = Messenger(Handler(Looper.getMainLooper(), ::onClientMessage))
|
||||
private var clientMessenger: Messenger? = null
|
||||
|
||||
private val paths = mutableListOf<PathTag>()
|
||||
private val servers = mutableListOf<String>()
|
||||
private var author = ""
|
||||
private var identifier = ""
|
||||
private var launchToken = ""
|
||||
private var websiteMode = false
|
||||
private var useTor = true
|
||||
private var proxyPort = -1
|
||||
private var bgColor = android.graphics.Color.WHITE
|
||||
private var declaredDomains: List<String> = emptyList()
|
||||
/**
|
||||
* Per-embedded-tab state. A single NappletHostService instance is shared by every embedded
|
||||
* napplet/nSite tab (they bind the same Intent), so each tab's config, content server, WebView, and
|
||||
* broker bridge live here, keyed by [sessionId]. Each tab has its OWN [replyMessenger] so broker
|
||||
* responses AND unsolicited pushes come back tagged to the right tab (the broker echoes replyTo).
|
||||
*/
|
||||
private inner class NappletTab(
|
||||
val sessionId: String,
|
||||
var clientMessenger: Messenger?,
|
||||
val paths: List<PathTag>,
|
||||
val servers: List<String>,
|
||||
val author: String,
|
||||
val identifier: String,
|
||||
val launchToken: String,
|
||||
val websiteMode: Boolean,
|
||||
val useTor: Boolean,
|
||||
val proxyPort: Int,
|
||||
val bgColor: Int,
|
||||
val declaredDomains: List<String>,
|
||||
) {
|
||||
var contentServer: NappletContentServer? = null
|
||||
var webView: WebView? = null
|
||||
var bridgeReplyProxy: JavaScriptReplyProxy? = null
|
||||
var fireSeq = 0
|
||||
val replyMessenger = Messenger(Handler(Looper.getMainLooper()) { onBrokerReply(this, it) })
|
||||
}
|
||||
|
||||
private lateinit var contentServer: NappletContentServer
|
||||
private val tabs = mutableMapOf<String, NappletTab>()
|
||||
|
||||
private var webView: WebView? = null
|
||||
|
||||
// ---- broker bridge (identical trust model to NappletHostActivity: one launch token) ----
|
||||
// ---- broker bridge: ONE binding shared by all tabs; per-message replyTo scopes the routing ----
|
||||
private var brokerMessenger: Messenger? = null
|
||||
private val replyMessenger = Messenger(Handler(Looper.getMainLooper(), ::onBrokerReply))
|
||||
private var brokerBound = false
|
||||
private val pendingBrokerRequests = mutableListOf<Message>()
|
||||
private var bridgeReplyProxy: JavaScriptReplyProxy? = null
|
||||
private var fireSeq = 0
|
||||
|
||||
private val brokerConnection =
|
||||
object : ServiceConnection {
|
||||
@@ -115,71 +126,90 @@ class NappletHostService : Service() {
|
||||
override fun onBind(intent: Intent?): IBinder = incoming.binder
|
||||
|
||||
override fun onDestroy() {
|
||||
runCatching { unbindService(brokerConnection) }
|
||||
webView?.destroy()
|
||||
webView = null
|
||||
if (brokerBound) {
|
||||
runCatching { unbindService(brokerConnection) }
|
||||
brokerBound = false
|
||||
}
|
||||
tabs.values.forEach { it.webView?.destroy() }
|
||||
tabs.clear()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun tabFor(msg: Message): NappletTab? = msg.data?.getString(NappletEmbedContract.KEY_SESSION_ID)?.let { tabs[it] }
|
||||
|
||||
private fun tabFor(view: WebView): NappletTab? = tabs.values.firstOrNull { it.webView === view }
|
||||
|
||||
private fun onClientMessage(msg: Message): Boolean {
|
||||
when (msg.what) {
|
||||
NappletEmbedContract.MSG_CREATE_SESSION -> {
|
||||
if (!readParams(msg)) return true
|
||||
clientMessenger = msg.replyTo
|
||||
bindService(Intent().setClassName(this, NappletHostContract.BROKER_SERVICE_CLASS), brokerConnection, BIND_AUTO_CREATE)
|
||||
replyWithAdapter()
|
||||
val tab = buildTab(msg) ?: return true
|
||||
tabs[tab.sessionId] = tab
|
||||
// Bind the broker once; a re-sent MSG_CREATE_SESSION must not leak a second binding.
|
||||
if (!brokerBound) {
|
||||
brokerBound = bindService(Intent().setClassName(this, NappletHostContract.BROKER_SERVICE_CLASS), brokerConnection, BIND_AUTO_CREATE)
|
||||
}
|
||||
replyWithAdapter(tab)
|
||||
}
|
||||
NappletEmbedContract.MSG_BACK -> webView?.let { if (it.canGoBack()) it.goBack() }
|
||||
NappletEmbedContract.MSG_RELOAD -> webView?.reload()
|
||||
NappletEmbedContract.MSG_BACK -> tabFor(msg)?.webView?.let { if (it.canGoBack()) it.goBack() }
|
||||
NappletEmbedContract.MSG_RELOAD -> tabFor(msg)?.webView?.reload()
|
||||
NappletEmbedContract.MSG_PAUSE ->
|
||||
webView?.let {
|
||||
tabFor(msg)?.webView?.let {
|
||||
it.onPause()
|
||||
it.pauseTimers()
|
||||
}
|
||||
NappletEmbedContract.MSG_RESUME ->
|
||||
webView?.let {
|
||||
tabFor(msg)?.webView?.let {
|
||||
it.onResume()
|
||||
it.resumeTimers()
|
||||
}
|
||||
NappletEmbedContract.MSG_IME_OP -> {
|
||||
val tab = tabFor(msg) ?: return true
|
||||
val payload = msg.data?.getString(NappletEmbedContract.KEY_IME_PAYLOAD) ?: return true
|
||||
bridgeReplyProxy?.postMessage(payload)
|
||||
tab.bridgeReplyProxy?.postMessage(payload)
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun readParams(msg: Message): Boolean {
|
||||
val data = msg.data ?: return false
|
||||
val pathList = data.getStringArrayList(NappletHostContract.EXTRA_PATHS) ?: return false
|
||||
val hashList = data.getStringArrayList(NappletHostContract.EXTRA_HASHES) ?: return false
|
||||
if (pathList.size != hashList.size || pathList.isEmpty()) return false
|
||||
for (i in pathList.indices) paths.add(PathTag(pathList[i], hashList[i]))
|
||||
servers.addAll(data.getStringArrayList(NappletHostContract.EXTRA_SERVERS) ?: emptyList())
|
||||
author = data.getString(NappletHostContract.EXTRA_AUTHOR).orEmpty()
|
||||
identifier = data.getString(NappletHostContract.EXTRA_IDENTIFIER).orEmpty()
|
||||
websiteMode = data.getBoolean(NappletHostContract.EXTRA_WEBSITE_MODE, false)
|
||||
useTor = data.getBoolean(NappletHostContract.EXTRA_USE_TOR, true)
|
||||
proxyPort = data.getInt(NappletHostContract.EXTRA_PROXY_PORT, -1)
|
||||
bgColor = data.getInt(NappletHostContract.EXTRA_BG_COLOR, android.graphics.Color.WHITE)
|
||||
launchToken = data.getString(NappletHostContract.EXTRA_LAUNCH_TOKEN).orEmpty()
|
||||
private fun buildTab(msg: Message): NappletTab? {
|
||||
val data = msg.data ?: return null
|
||||
val sessionId = data.getString(NappletEmbedContract.KEY_SESSION_ID) ?: return null
|
||||
val pathList = data.getStringArrayList(NappletHostContract.EXTRA_PATHS) ?: return null
|
||||
val hashList = data.getStringArrayList(NappletHostContract.EXTRA_HASHES) ?: return null
|
||||
if (pathList.size != hashList.size || pathList.isEmpty()) return null
|
||||
val author = data.getString(NappletHostContract.EXTRA_AUTHOR).orEmpty()
|
||||
val launchToken = data.getString(NappletHostContract.EXTRA_LAUNCH_TOKEN).orEmpty()
|
||||
if (author.isEmpty() || launchToken.isEmpty()) return null
|
||||
|
||||
val requires = data.getStringArrayList(NappletHostContract.EXTRA_REQUIRES) ?: emptyList()
|
||||
declaredDomains = (listOf("shell") + resolveRequiredCapabilities(requires).capabilities.map { it.name.lowercase() }).distinct()
|
||||
val declaredDomains = (listOf("shell") + resolveRequiredCapabilities(requires).capabilities.map { it.name.lowercase() }).distinct()
|
||||
|
||||
return author.isNotEmpty() && launchToken.isNotEmpty()
|
||||
return NappletTab(
|
||||
sessionId = sessionId,
|
||||
clientMessenger = msg.replyTo,
|
||||
paths = pathList.indices.map { PathTag(pathList[it], hashList[it]) },
|
||||
servers = data.getStringArrayList(NappletHostContract.EXTRA_SERVERS) ?: emptyList(),
|
||||
author = author,
|
||||
identifier = data.getString(NappletHostContract.EXTRA_IDENTIFIER).orEmpty(),
|
||||
launchToken = launchToken,
|
||||
websiteMode = data.getBoolean(NappletHostContract.EXTRA_WEBSITE_MODE, false),
|
||||
useTor = data.getBoolean(NappletHostContract.EXTRA_USE_TOR, true),
|
||||
proxyPort = data.getInt(NappletHostContract.EXTRA_PROXY_PORT, -1),
|
||||
bgColor = data.getInt(NappletHostContract.EXTRA_BG_COLOR, android.graphics.Color.WHITE),
|
||||
declaredDomains = declaredDomains,
|
||||
)
|
||||
}
|
||||
|
||||
/** Builds the SandboxedUiAdapter and ships its cross-process handle (coreLibInfo) to the client. */
|
||||
private fun replyWithAdapter() {
|
||||
val adapter = NappletHostUiAdapter(this)
|
||||
/** Builds the SandboxedUiAdapter for [tab] and ships its cross-process handle (coreLibInfo) to the client. */
|
||||
private fun replyWithAdapter(tab: NappletTab) {
|
||||
val adapter = NappletHostUiAdapter(this, tab.sessionId)
|
||||
val coreLibInfo = adapter.toCoreLibInfo(this)
|
||||
val reply =
|
||||
Message.obtain(null, NappletEmbedContract.MSG_SESSION_READY).apply {
|
||||
data = Bundle().apply { putBundle(NappletEmbedContract.KEY_CORE_LIB_INFO, coreLibInfo) }
|
||||
}
|
||||
runCatching { clientMessenger?.send(reply) }
|
||||
runCatching { tab.clientMessenger?.send(reply) }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,38 +217,39 @@ class NappletHostService : Service() {
|
||||
* surface). Mirrors [NappletHostActivity]: serves the shell + verified blobs through the content
|
||||
* server, installs the origin-restricted shell bridge, loads the trusted shell URL.
|
||||
*/
|
||||
fun createHostWebView(context: Context): WebView {
|
||||
// NOTE: a single NappletHostService instance is shared by every embedded napplet tab (they bind
|
||||
// the same Intent), but each tab opens its own session with its own WebView; [webView] is just a
|
||||
// "latest" pointer. Do NOT destroy it here — that would tear down a *sibling* tab's live WebView
|
||||
// and black it out. Each session owns its WebView until it closes.
|
||||
fun createHostWebView(
|
||||
context: Context,
|
||||
sessionId: String,
|
||||
): WebView {
|
||||
val wv = WebView(context)
|
||||
val tab = tabs[sessionId] ?: return wv
|
||||
val shellHtml = readContractAsset(NappletWebContract.SHELL_HTML_PATH)
|
||||
val shim = readContractAsset(NappletWebContract.SHIM_JS_PATH).decodeToString()
|
||||
val appOrigin = NappletWebContract.appOrigin(deriveAppId(author, identifier))
|
||||
val effectiveProxy = if (useTor) proxyPort else -1
|
||||
contentServer = NappletContentServer(paths, servers, effectiveProxy, cacheDir, shellHtml, shim, appOrigin, websiteMode, imeProxy = true)
|
||||
val appOrigin = NappletWebContract.appOrigin(deriveAppId(tab.author, tab.identifier))
|
||||
val effectiveProxy = if (tab.useTor) tab.proxyPort else -1
|
||||
tab.contentServer = NappletContentServer(tab.paths, tab.servers, effectiveProxy, cacheDir, shellHtml, shim, appOrigin, tab.websiteMode, imeProxy = true)
|
||||
|
||||
val wv = WebView(context)
|
||||
hardenWebView(wv)
|
||||
hardenWebView(wv, tab)
|
||||
// Theme the pre-load background so the shell/app loading shows Amethyst's background, not white.
|
||||
wv.setBackgroundColor(bgColor)
|
||||
wv.setBackgroundColor(tab.bgColor)
|
||||
wv.dropSystemBarInsets()
|
||||
if (websiteMode) applyWebViewProxy(effectiveProxy)
|
||||
if (tab.websiteMode) applyWebViewProxy(effectiveProxy)
|
||||
WebViewCompat.addWebMessageListener(wv, NappletWebContract.BRIDGE_NAME, setOf(NappletWebContract.ORIGIN), ::onShellMessage)
|
||||
webView = wv
|
||||
tab.webView = wv
|
||||
wv.loadUrl(NappletWebContract.SHELL_URL)
|
||||
return wv
|
||||
}
|
||||
|
||||
/** A session closed: destroy that session's own WebView and clear the shared pointer only if it
|
||||
* still referenced it (a sibling tab may have become the latest). */
|
||||
fun onSessionClosed(closed: WebView) {
|
||||
if (webView === closed) webView = null
|
||||
closed.destroy()
|
||||
/** A session closed: drop the tab and destroy its own WebView (never a sibling's). */
|
||||
fun onSessionClosed(sessionId: String) {
|
||||
tabs.remove(sessionId)?.webView?.destroy()
|
||||
}
|
||||
|
||||
@Suppress("SetJavaScriptEnabled")
|
||||
private fun hardenWebView(wv: WebView) {
|
||||
private fun hardenWebView(
|
||||
wv: WebView,
|
||||
tab: NappletTab,
|
||||
) {
|
||||
wv.settings.apply {
|
||||
javaScriptEnabled = true
|
||||
domStorageEnabled = true
|
||||
@@ -241,7 +272,7 @@ class NappletHostService : Service() {
|
||||
}
|
||||
wv.overScrollMode = View.OVER_SCROLL_NEVER
|
||||
WebView.setWebContentsDebuggingEnabled(false)
|
||||
wv.webViewClient = HostClient()
|
||||
wv.webViewClient = HostClient(tab)
|
||||
}
|
||||
|
||||
private fun applyWebViewProxy(port: Int) {
|
||||
@@ -258,22 +289,24 @@ class NappletHostService : Service() {
|
||||
}
|
||||
|
||||
/** Serves only the trusted shell and the manifest's verified blobs; external links go to the system. */
|
||||
private inner class HostClient : WebViewClient() {
|
||||
private inner class HostClient(
|
||||
private val tab: NappletTab,
|
||||
) : WebViewClient() {
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
): WebResourceResponse? = contentServer.serve(request)
|
||||
): WebResourceResponse? = tab.contentServer?.serve(request)
|
||||
|
||||
override fun doUpdateVisitedHistory(
|
||||
view: WebView,
|
||||
url: String,
|
||||
isReload: Boolean,
|
||||
) = pushState(view)
|
||||
) = pushState(tab, view)
|
||||
|
||||
override fun onPageFinished(
|
||||
view: WebView,
|
||||
url: String,
|
||||
) = pushState(view)
|
||||
) = pushState(tab, view)
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
@@ -288,12 +321,15 @@ class NappletHostService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun pushState(view: WebView) {
|
||||
private fun pushState(
|
||||
tab: NappletTab,
|
||||
view: WebView,
|
||||
) {
|
||||
val message =
|
||||
Message.obtain(null, NappletEmbedContract.MSG_STATE).apply {
|
||||
data = Bundle().apply { putBoolean(NappletEmbedContract.KEY_CAN_GO_BACK, view.canGoBack()) }
|
||||
}
|
||||
runCatching { clientMessenger?.send(message) }
|
||||
runCatching { tab.clientMessenger?.send(message) }
|
||||
}
|
||||
|
||||
// ---- bridge: shell <-> native (mirror of NappletHostActivity.onShellMessage) ----
|
||||
@@ -306,13 +342,14 @@ class NappletHostService : Service() {
|
||||
replyProxy: JavaScriptReplyProxy,
|
||||
) {
|
||||
if (!isMainFrame) return
|
||||
bridgeReplyProxy = replyProxy
|
||||
val tab = tabFor(view) ?: return
|
||||
tab.bridgeReplyProxy = replyProxy
|
||||
|
||||
val raw = message.data ?: return
|
||||
val envelope = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
||||
|
||||
if (envelope.optString("type") == "shell.ready") {
|
||||
runCatching { replyProxy.postMessage(NappletProtocolJson.encodeShellInit(declaredDomains, declaredDomains)) }
|
||||
runCatching { replyProxy.postMessage(NappletProtocolJson.encodeShellInit(tab.declaredDomains, tab.declaredDomains)) }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -322,19 +359,19 @@ class NappletHostService : Service() {
|
||||
Message.obtain(null, NappletEmbedContract.MSG_IME_EVENT).apply {
|
||||
data = Bundle().apply { putString(NappletEmbedContract.KEY_IME_PAYLOAD, raw) }
|
||||
}
|
||||
runCatching { clientMessenger?.send(reply) }
|
||||
runCatching { tab.clientMessenger?.send(reply) }
|
||||
return
|
||||
}
|
||||
|
||||
val id = envelope.optString("id").ifEmpty { "fire-${fireSeq++}" }
|
||||
val id = envelope.optString("id").ifEmpty { "fire-${tab.fireSeq++}" }
|
||||
val msg =
|
||||
Message.obtain(null, NappletIpc.MSG_REQUEST).apply {
|
||||
replyTo = replyMessenger
|
||||
replyTo = tab.replyMessenger
|
||||
data =
|
||||
Bundle().apply {
|
||||
putString(NappletIpc.KEY_REQUEST_ID, id)
|
||||
putString(NappletIpc.KEY_PAYLOAD, raw)
|
||||
putString(NappletIpc.KEY_LAUNCH_TOKEN, launchToken)
|
||||
putString(NappletIpc.KEY_LAUNCH_TOKEN, tab.launchToken)
|
||||
}
|
||||
}
|
||||
if (brokerMessenger == null) pendingBrokerRequests.add(msg) else sendToBroker(msg)
|
||||
@@ -348,7 +385,11 @@ class NappletHostService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun onBrokerReply(msg: Message): Boolean {
|
||||
/** Broker reply for [tab] — delivered to the tab's own reply Messenger, so it's already scoped. */
|
||||
private fun onBrokerReply(
|
||||
tab: NappletTab,
|
||||
msg: Message,
|
||||
): Boolean {
|
||||
val data = msg.data ?: return true
|
||||
when (msg.what) {
|
||||
NappletIpc.MSG_RESPONSE -> {
|
||||
@@ -356,12 +397,12 @@ class NappletHostService : Service() {
|
||||
val payload = data.getString(NappletIpc.KEY_PAYLOAD) ?: return true
|
||||
val result = runCatching { JSONObject(payload) }.getOrNull() ?: JSONObject()
|
||||
result.put("id", id)
|
||||
notifyIfSensitive(result)
|
||||
bridgeReplyProxy?.postMessage(result.toString())
|
||||
notifyIfSensitive(tab, result)
|
||||
tab.bridgeReplyProxy?.postMessage(result.toString())
|
||||
}
|
||||
NappletIpc.MSG_PUSH -> {
|
||||
val payload = data.getString(NappletIpc.KEY_PAYLOAD) ?: return true
|
||||
bridgeReplyProxy?.postMessage(payload)
|
||||
tab.bridgeReplyProxy?.postMessage(payload)
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
@@ -369,7 +410,10 @@ class NappletHostService : Service() {
|
||||
}
|
||||
|
||||
/** Pushes a notice to the main process for a granted "allow always" sensitive op, so it can toast. */
|
||||
private fun notifyIfSensitive(result: JSONObject) {
|
||||
private fun notifyIfSensitive(
|
||||
tab: NappletTab,
|
||||
result: JSONObject,
|
||||
) {
|
||||
if (!result.optBoolean("ok")) return
|
||||
val notice =
|
||||
when (result.optString("type")) {
|
||||
@@ -382,7 +426,7 @@ class NappletHostService : Service() {
|
||||
Message.obtain(null, NappletEmbedContract.MSG_NOTICE).apply {
|
||||
data = Bundle().apply { putString(NappletEmbedContract.KEY_NOTICE, notice) }
|
||||
}
|
||||
runCatching { clientMessenger?.send(message) }
|
||||
runCatching { tab.clientMessenger?.send(message) }
|
||||
}
|
||||
|
||||
private fun readContractAsset(path: String): ByteArray = assets.open(NappletWebContract.RESOURCE_ASSET_ROOT + path).use { it.readBytes() }
|
||||
|
||||
+5
-3
@@ -44,6 +44,7 @@ import java.util.concurrent.Executor
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
class NappletHostUiAdapter(
|
||||
private val service: NappletHostService,
|
||||
private val sessionId: String,
|
||||
) : AbstractSandboxedUiAdapter() {
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
@@ -59,11 +60,11 @@ class NappletHostUiAdapter(
|
||||
// WebView creation must run on the main thread; openSession is called on a binder thread.
|
||||
mainHandler.post {
|
||||
runCatching {
|
||||
val webView = service.createHostWebView(context)
|
||||
val webView = service.createHostWebView(context, sessionId)
|
||||
// FrameLayout.LayoutParams (a MarginLayoutParams) — the SurfaceControlViewHost container
|
||||
// measures children with measureChildWithMargins, which casts to MarginLayoutParams.
|
||||
webView.layoutParams = FrameLayout.LayoutParams(initialWidth, initialHeight)
|
||||
HostSession(webView, service)
|
||||
HostSession(sessionId, webView, service)
|
||||
}.onSuccess { session -> clientExecutor.execute { client.onSessionOpened(session) } }
|
||||
.onFailure { t -> clientExecutor.execute { client.onSessionError(t) } }
|
||||
}
|
||||
@@ -73,6 +74,7 @@ class NappletHostUiAdapter(
|
||||
/** A single embedded napplet/nsite session: the WebView is the rendered view; close tears it down. */
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private class HostSession(
|
||||
private val sessionId: String,
|
||||
private val webView: WebView,
|
||||
private val service: NappletHostService,
|
||||
) : SandboxedUiAdapter.Session {
|
||||
@@ -97,6 +99,6 @@ private class HostSession(
|
||||
override fun notifyUiChanged(uiContainerInfo: Bundle) {}
|
||||
|
||||
override fun close() {
|
||||
service.onSessionClosed(webView)
|
||||
service.onSessionClosed(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user