mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
refactor: replace org.json with kotlinx.serialization in source files
Sweeps the last org.json usages out of the Kotlin sources and moves them to kotlinx.serialization's JSON tree API (already the project standard): - nappletHost: bridge/broker envelope handling in NappletHostActivity, NappletHostService, NappletBrowserActivity, NappletBrowserService and NappletFaviconSniffer now parses with Json.parseToJsonElement via new total helpers in JsonEnvelope.kt (absent/mistyped fields degrade to empty/false instead of throwing, matching the old opt* semantics). Adds the kotlinx-serialization-json runtime to the module (tree API only, so no serialization plugin needed). - amethyst embed IME relay: EmbeddedImeBridge parses ime.* envelopes with JsonObject accessors; RemoteImeView and EmbeddedTabLayer build their outgoing envelopes with buildJsonObject. - tools/ime-test: drops the now-stale "org.json is stubbed in JVM unit tests" rationale from the README and shim-events.mjs header. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVcZwp65oybotmq5o66foW
This commit is contained in:
+56
-36
@@ -20,25 +20,45 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.embed
|
||||
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.intOrNull
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
// The `ime.*` envelopes are ad-hoc JSON from the page shim; every field is optional, so each
|
||||
// accessor degrades to null rather than throwing on an absent or mistyped value.
|
||||
private fun JsonObject.string(key: String): String? = (this[key] as? JsonPrimitive)?.contentOrNull
|
||||
|
||||
private fun JsonObject.int(key: String): Int? = (this[key] as? JsonPrimitive)?.intOrNull
|
||||
|
||||
private fun JsonObject.double(key: String): Double? = (this[key] as? JsonPrimitive)?.doubleOrNull
|
||||
|
||||
private fun JsonObject.bool(key: String): Boolean? = (this[key] as? JsonPrimitive)?.booleanOrNull
|
||||
|
||||
private fun JsonObject.obj(key: String): JsonObject? = this[key] as? JsonObject
|
||||
|
||||
/** Parses the `geom` object of an `ime.pagesel` payload into a [SelectionGeometry], or null if absent. */
|
||||
fun parseSelectionGeometry(o: JSONObject?): SelectionGeometry? {
|
||||
fun parseSelectionGeometry(o: JsonObject?): SelectionGeometry? {
|
||||
if (o == null) return null
|
||||
return SelectionGeometry(
|
||||
left = o.optDouble("l", 0.0).toFloat(),
|
||||
top = o.optDouble("t", 0.0).toFloat(),
|
||||
right = o.optDouble("r", 0.0).toFloat(),
|
||||
bottom = o.optDouble("b", 0.0).toFloat(),
|
||||
startX = o.optDouble("sx", 0.0).toFloat(),
|
||||
startBottom = o.optDouble("sb", 0.0).toFloat(),
|
||||
endX = o.optDouble("ex", 0.0).toFloat(),
|
||||
endBottom = o.optDouble("eb", 0.0).toFloat(),
|
||||
viewportWidth = o.optDouble("vw", 0.0).toFloat(),
|
||||
caretX = if (o.has("cx")) o.optDouble("cx").toFloat() else null,
|
||||
caretTop = if (o.has("ct")) o.optDouble("ct").toFloat() else null,
|
||||
caretBottom = if (o.has("cb")) o.optDouble("cb").toFloat() else null,
|
||||
isRange = o.optBoolean("rng", false),
|
||||
left = (o.double("l") ?: 0.0).toFloat(),
|
||||
top = (o.double("t") ?: 0.0).toFloat(),
|
||||
right = (o.double("r") ?: 0.0).toFloat(),
|
||||
bottom = (o.double("b") ?: 0.0).toFloat(),
|
||||
startX = (o.double("sx") ?: 0.0).toFloat(),
|
||||
startBottom = (o.double("sb") ?: 0.0).toFloat(),
|
||||
endX = (o.double("ex") ?: 0.0).toFloat(),
|
||||
endBottom = (o.double("eb") ?: 0.0).toFloat(),
|
||||
viewportWidth = (o.double("vw") ?: 0.0).toFloat(),
|
||||
caretX = o.double("cx")?.toFloat(),
|
||||
caretTop = o.double("ct")?.toFloat(),
|
||||
caretBottom = o.double("cb")?.toFloat(),
|
||||
isRange = o.bool("rng") ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,45 +82,45 @@ interface EmbeddedImeBridge {
|
||||
* learns there is a field to put the keyboard back on. Sent when a tab becomes the active one again, and when
|
||||
* an [ImeEvent.WantKeyboard] tap arrives for a field this host no longer mirrors.
|
||||
*/
|
||||
fun EmbeddedImeBridge.requestImeResync() = sendImeOp(JSONObject().put("type", "ime.resync").toString())
|
||||
fun EmbeddedImeBridge.requestImeResync() = sendImeOp(buildJsonObject { put("type", "ime.resync") }.toString())
|
||||
|
||||
/** Parses one page → host `ime.*` envelope into an [ImeEvent], or null for anything unrecognized. */
|
||||
fun parseImeEvent(payload: String): ImeEvent? {
|
||||
val o = runCatching { JSONObject(payload) }.getOrNull() ?: return null
|
||||
return when (o.optString("type")) {
|
||||
val o = runCatching { Json.parseToJsonElement(payload) as? JsonObject }.getOrNull() ?: return null
|
||||
return when (o.string("type")) {
|
||||
"ime.focus" -> parseFocus(o)
|
||||
"ime.wantkb" -> ImeEvent.WantKeyboard
|
||||
"ime.refocus" -> ImeEvent.ReFocus(parseFocus(o))
|
||||
"ime.blur" -> ImeEvent.Blur
|
||||
"ime.state" ->
|
||||
ImeEvent.State(
|
||||
text = o.optString("text", ""),
|
||||
selStart = o.optInt("selStart", 0),
|
||||
selEnd = o.optInt("selEnd", 0),
|
||||
geometry = parseSelectionGeometry(o.optJSONObject("geom")),
|
||||
text = o.string("text") ?: "",
|
||||
selStart = o.int("selStart") ?: 0,
|
||||
selEnd = o.int("selEnd") ?: 0,
|
||||
geometry = parseSelectionGeometry(o.obj("geom")),
|
||||
)
|
||||
"ime.pagesel" ->
|
||||
ImeEvent.PageSelection(
|
||||
active = o.optBoolean("active", false),
|
||||
text = o.optString("text", ""),
|
||||
geometry = parseSelectionGeometry(o.optJSONObject("geom")),
|
||||
active = o.bool("active") ?: false,
|
||||
text = o.string("text") ?: "",
|
||||
geometry = parseSelectionGeometry(o.obj("geom")),
|
||||
)
|
||||
"ime.scroll" -> ImeEvent.Scroll(active = o.optBoolean("active", false))
|
||||
"ime.carettap" -> ImeEvent.CaretTap(geometry = parseSelectionGeometry(o.optJSONObject("geom")))
|
||||
"ime.scroll" -> ImeEvent.Scroll(active = o.bool("active") ?: false)
|
||||
"ime.carettap" -> ImeEvent.CaretTap(geometry = parseSelectionGeometry(o.obj("geom")))
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseFocus(o: JSONObject) =
|
||||
private fun parseFocus(o: JsonObject) =
|
||||
ImeEvent.Focus(
|
||||
inputType = o.optString("inputType", "text"),
|
||||
enterKeyHint = o.optString("enterKeyHint", ""),
|
||||
multiline = o.optBoolean("multiline", false),
|
||||
readOnly = o.optBoolean("readOnly", false),
|
||||
text = o.optString("text", ""),
|
||||
selStart = o.optInt("selStart", 0),
|
||||
selEnd = o.optInt("selEnd", 0),
|
||||
geometry = parseSelectionGeometry(o.optJSONObject("geom")),
|
||||
inputType = o.string("inputType") ?: "text",
|
||||
enterKeyHint = o.string("enterKeyHint") ?: "",
|
||||
multiline = o.bool("multiline") ?: false,
|
||||
readOnly = o.bool("readOnly") ?: false,
|
||||
text = o.string("text") ?: "",
|
||||
selStart = o.int("selStart") ?: 0,
|
||||
selEnd = o.int("selEnd") ?: 0,
|
||||
geometry = parseSelectionGeometry(o.obj("geom")),
|
||||
)
|
||||
|
||||
/** What the focused page field reports up to the host keyboard. */
|
||||
|
||||
+16
-8
@@ -83,7 +83,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.privacysandbox.ui.client.view.SandboxedSdkView
|
||||
import kotlinx.coroutines.delay
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// How far off-screen a parked (inactive) warm tab is shifted — well past any real screen width.
|
||||
@@ -106,12 +107,12 @@ private fun EmbeddedImeBridge.sendFieldOp(
|
||||
cssX: Float,
|
||||
cssY: Float,
|
||||
) = sendImeOp(
|
||||
JSONObject()
|
||||
.put("type", type)
|
||||
.apply { if (edge != null) put("edge", edge) }
|
||||
.put("x", cssX.toDouble())
|
||||
.put("y", cssY.toDouble())
|
||||
.toString(),
|
||||
buildJsonObject {
|
||||
put("type", type)
|
||||
if (edge != null) put("edge", edge)
|
||||
put("x", cssX.toDouble())
|
||||
put("y", cssY.toDouble())
|
||||
}.toString(),
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -508,7 +509,14 @@ fun EmbeddedTabLayer(barFavoriteIds: List<String>) {
|
||||
fingerPx.y > oy + bounds.height - edgeZonePx -> AUTOSCROLL_STEP_CSS
|
||||
else -> 0.0
|
||||
}
|
||||
if (dy != 0.0) imeBridge?.sendImeOp(JSONObject().put("type", "ime.autoscroll").put("dy", dy).toString())
|
||||
if (dy != 0.0) {
|
||||
imeBridge?.sendImeOp(
|
||||
buildJsonObject {
|
||||
put("type", "ime.autoscroll")
|
||||
put("dy", dy)
|
||||
}.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!active || magProbe == null) {
|
||||
magnifier.hide()
|
||||
|
||||
+13
-10
@@ -32,7 +32,9 @@ import android.view.inputmethod.InputConnection
|
||||
import android.view.inputmethod.InputConnectionWrapper
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.EditText
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
|
||||
/**
|
||||
* The main-app-window home for the soft keyboard when a field is focused inside an embedded WebView. The
|
||||
@@ -124,7 +126,7 @@ class RemoteImeView(
|
||||
)
|
||||
// The IME's "Go/Search/Send/Done" — the page submits/handles it (single-line has no newline).
|
||||
setOnEditorActionListener { _, _, _ ->
|
||||
bridge?.sendImeOp(JSONObject().put("type", "ime.action").toString())
|
||||
bridge?.sendImeOp(buildJsonObject { put("type", "ime.action") }.toString())
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -365,12 +367,12 @@ class RemoteImeView(
|
||||
schedule()
|
||||
}
|
||||
|
||||
private fun stateJson(): JSONObject {
|
||||
private fun stateJson(): JsonObject {
|
||||
val editable = text
|
||||
val composingStart = if (editable != null) BaseInputConnection.getComposingSpanStart(editable) else -1
|
||||
val composingEnd = if (editable != null) BaseInputConnection.getComposingSpanEnd(editable) else -1
|
||||
return JSONObject()
|
||||
.put("type", "ime.set")
|
||||
return buildJsonObject {
|
||||
put("type", "ime.set")
|
||||
// A readonly field's text must never travel back to the page. TYPE_NULL keeps the *user* from
|
||||
// typing into the mirror, but the mirror still flushes on selection changes — a long-press
|
||||
// select-all, then Chrome's collapse-to-endpoint, both emit one — and that flush is delivered
|
||||
@@ -381,11 +383,12 @@ class RemoteImeView(
|
||||
// Omitting the key (rather than sending the current text) makes the shim treat the message as
|
||||
// selection-only — `var next = (msg.text != null) ? String(msg.text) : prev` — so the
|
||||
// host-drawn handles and Copy keep working off a synced selection while nothing can be written.
|
||||
.apply { if (!fieldReadOnly) put("text", editable?.toString() ?: "") }
|
||||
.put("selStart", selectionStart)
|
||||
.put("selEnd", selectionEnd)
|
||||
.put("composingStart", composingStart)
|
||||
.put("composingEnd", composingEnd)
|
||||
if (!fieldReadOnly) put("text", editable?.toString() ?: "")
|
||||
put("selStart", selectionStart)
|
||||
put("selEnd", selectionEnd)
|
||||
put("composingStart", composingStart)
|
||||
put("composingEnd", composingEnd)
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushState() {
|
||||
|
||||
@@ -44,6 +44,9 @@ dependencies {
|
||||
implementation(libs.androidx.webkit)
|
||||
implementation(libs.okhttp)
|
||||
|
||||
// Tree-level JSON for the bridge/broker envelopes (no @Serializable codegen, so no plugin needed).
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
// Provider side of the cross-process UI embedding: hosts the browser WebView in this keyless
|
||||
// `:napplet` process and ships its rendered surface to the main app via SurfaceControlViewHost.
|
||||
implementation(libs.androidx.privacysandbox.ui.core)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.napplethost
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.contentOrNull
|
||||
|
||||
// Tree-level JSON helpers (kotlinx.serialization) for the ad-hoc bridge/broker envelopes this
|
||||
// process relays ({type, id, ...}). The envelopes come from untrusted pages, so every accessor
|
||||
// is total: absent/mistyped fields degrade to the empty/false default instead of throwing.
|
||||
|
||||
/** Parses [raw] as a JSON object, or null when it is malformed or not an object. */
|
||||
internal fun parseJsonObject(raw: String): JsonObject? = runCatching { Json.parseToJsonElement(raw) as? JsonObject }.getOrNull()
|
||||
|
||||
/** The value at [key] rendered as a string, or "" when absent, null, or not a primitive. */
|
||||
internal fun JsonObject.stringOrEmpty(key: String): String = (this[key] as? JsonPrimitive)?.contentOrNull ?: ""
|
||||
|
||||
/** The value at [key] as a boolean, or false when absent or not a boolean. */
|
||||
internal fun JsonObject.booleanOrFalse(key: String): Boolean = (this[key] as? JsonPrimitive)?.booleanOrNull ?: false
|
||||
|
||||
/** A copy of this envelope with [key] set to [value] ([JsonObject] is immutable). */
|
||||
internal fun JsonObject.withString(
|
||||
key: String,
|
||||
value: String,
|
||||
): JsonObject = JsonObject(this + (key to JsonPrimitive(value)))
|
||||
+4
-5
@@ -64,7 +64,7 @@ import androidx.webkit.WebViewFeature
|
||||
import com.vitorpamplona.amethyst.commons.browser.OmniboxInput
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executor
|
||||
@@ -616,13 +616,13 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
if (!isMainFrame) return
|
||||
bridgeReplyProxy = replyProxy
|
||||
val raw = message.data ?: return
|
||||
val envelope = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
||||
val envelope = parseJsonObject(raw) ?: return
|
||||
|
||||
val scheme = sourceOrigin.scheme ?: return
|
||||
val host = sourceOrigin.host ?: return
|
||||
val origin = "$scheme://$host" + if (sourceOrigin.port > 0) ":${sourceOrigin.port}" else ""
|
||||
|
||||
val id = envelope.optString("id").ifEmpty { "fire-${fireSeq++}" }
|
||||
val id = envelope.stringOrEmpty("id").ifEmpty { "fire-${fireSeq++}" }
|
||||
val msg =
|
||||
Message.obtain(null, NappletIpc.MSG_REQUEST).apply {
|
||||
replyTo = replyMessenger
|
||||
@@ -667,8 +667,7 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
NappletIpc.MSG_RESPONSE -> {
|
||||
val id = data.getString(NappletIpc.KEY_REQUEST_ID) ?: return true
|
||||
val payload = data.getString(NappletIpc.KEY_PAYLOAD) ?: return true
|
||||
val result = runCatching { JSONObject(payload) }.getOrNull() ?: JSONObject()
|
||||
result.put("id", id)
|
||||
val result = (parseJsonObject(payload) ?: JsonObject(emptyMap())).withString("id", id)
|
||||
bridgeReplyProxy?.postMessage(result.toString())
|
||||
}
|
||||
NappletIpc.MSG_PUSH -> {
|
||||
|
||||
+5
-6
@@ -56,7 +56,7 @@ import androidx.webkit.WebViewFeature
|
||||
import com.vitorpamplona.amethyst.commons.browser.OmniboxInput
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletWebContract
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
@@ -591,10 +591,10 @@ class NappletBrowserService : Service() {
|
||||
if (!isMainFrame) return
|
||||
tab.bridgeReplyProxy = replyProxy
|
||||
val raw = message.data ?: return
|
||||
val envelope = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
||||
val envelope = parseJsonObject(raw) ?: return
|
||||
|
||||
// IME events aren't brokered — the main app hosts the keyboard. Relay the envelope to the client.
|
||||
if (envelope.optString("type").startsWith("ime.")) {
|
||||
if (envelope.stringOrEmpty("type").startsWith("ime.")) {
|
||||
val reply =
|
||||
Message.obtain(null, NappletBrowserContract.MSG_IME_EVENT).apply {
|
||||
data = Bundle().apply { putString(NappletBrowserContract.KEY_IME_PAYLOAD, raw) }
|
||||
@@ -607,7 +607,7 @@ class NappletBrowserService : Service() {
|
||||
val host = sourceOrigin.host ?: return
|
||||
val origin = "$scheme://$host" + if (sourceOrigin.port > 0) ":${sourceOrigin.port}" else ""
|
||||
|
||||
val id = envelope.optString("id").ifEmpty { "fire-${tab.fireSeq++}" }
|
||||
val id = envelope.stringOrEmpty("id").ifEmpty { "fire-${tab.fireSeq++}" }
|
||||
val msg =
|
||||
Message.obtain(null, NappletIpc.MSG_REQUEST).apply {
|
||||
replyTo = tab.replyMessenger
|
||||
@@ -716,8 +716,7 @@ class NappletBrowserService : Service() {
|
||||
NappletIpc.MSG_RESPONSE -> {
|
||||
val id = data.getString(NappletIpc.KEY_REQUEST_ID) ?: return true
|
||||
val payload = data.getString(NappletIpc.KEY_PAYLOAD) ?: return true
|
||||
val result = runCatching { JSONObject(payload) }.getOrNull() ?: JSONObject()
|
||||
result.put("id", id)
|
||||
val result = (parseJsonObject(payload) ?: JsonObject(emptyMap())).withString("id", id)
|
||||
runCatching { tab.bridgeReplyProxy?.postMessage(result.toString()) }
|
||||
}
|
||||
NappletIpc.MSG_PUSH -> {
|
||||
|
||||
+3
-4
@@ -26,7 +26,6 @@ import android.util.Base64
|
||||
import android.webkit.WebView
|
||||
import com.vitorpamplona.amethyst.commons.browser.OmniboxInput
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* Declared-favicon capture for the sandboxed browser WebViews, complementing
|
||||
@@ -111,9 +110,9 @@ internal object NappletFaviconSniffer {
|
||||
/** `state` to base64 payload, or null when the page has not produced a result for this seq yet. */
|
||||
private fun parse(raw: String?): Pair<String, String>? {
|
||||
if (raw == null || raw == "null") return null
|
||||
val json = runCatching { JSONObject(raw) }.getOrNull() ?: return null
|
||||
val state = json.optString("state").ifBlank { return null }
|
||||
return state to json.optString("data")
|
||||
val json = parseJsonObject(raw) ?: return null
|
||||
val state = json.stringOrEmpty("state").ifBlank { return null }
|
||||
return state to json.stringOrEmpty("data")
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+12
-13
@@ -78,7 +78,7 @@ import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executor
|
||||
import com.vitorpamplona.amethyst.commons.R as CommonsR
|
||||
@@ -744,17 +744,17 @@ class NappletHostActivity : ComponentActivity() {
|
||||
val raw = message.data ?: return
|
||||
// The applet sends a full upstream envelope {type, id, ...}; we forward it verbatim and
|
||||
// correlate on its id. The broker reads `type` to decode and to build the .result reply.
|
||||
val envelope = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
||||
val envelope = parseJsonObject(raw) ?: return
|
||||
|
||||
// Unbind a keyboard action as soon as the applet drops it (the broker's Done reply carries no
|
||||
// actionId, so the binding is removed here from the envelope itself).
|
||||
if (envelope.optString("type") == "keys.unregisterAction") {
|
||||
envelope.optString("actionId").takeIf { it.isNotEmpty() }?.let { keyActions.unregister(it) }
|
||||
if (envelope.stringOrEmpty("type") == "keys.unregisterAction") {
|
||||
envelope.stringOrEmpty("actionId").takeIf { it.isNotEmpty() }?.let { keyActions.unregister(it) }
|
||||
}
|
||||
|
||||
// Fire-and-forget messages (inc.emit, keys.unregisterAction) have no id; synthesize one so
|
||||
// they still reach the broker. Any reply is harmless — the applet has nothing to correlate.
|
||||
val id = envelope.optString("id").ifEmpty { "fire-${fireSeq++}" }
|
||||
val id = envelope.stringOrEmpty("id").ifEmpty { "fire-${fireSeq++}" }
|
||||
|
||||
val msg =
|
||||
Message.obtain(null, NappletIpc.MSG_REQUEST).apply {
|
||||
@@ -791,13 +791,12 @@ class NappletHostActivity : ComponentActivity() {
|
||||
val payload = data.getString(NappletIpc.KEY_PAYLOAD) ?: return true
|
||||
|
||||
// payload is the broker's {type:"...result", ok, ...}; inject the correlation id for the shim.
|
||||
val result = runCatching { JSONObject(payload) }.getOrNull() ?: JSONObject()
|
||||
result.put("id", id)
|
||||
val result = (parseJsonObject(payload) ?: JsonObject(emptyMap())).withString("id", id)
|
||||
// The broker authorized a keyboard action: bind the honored key combo so dispatchKeyEvent
|
||||
// can fire it. Only ok'd registrations bind (a denied KEYS request never reaches here).
|
||||
if (result.optString("type") == "keys.registerAction.result" && result.optBoolean("ok")) {
|
||||
val actionId = result.optString("actionId")
|
||||
if (actionId.isNotEmpty()) keyActions.register(actionId, result.optString("binding").ifEmpty { null })
|
||||
if (result.stringOrEmpty("type") == "keys.registerAction.result" && result.booleanOrFalse("ok")) {
|
||||
val actionId = result.stringOrEmpty("actionId")
|
||||
if (actionId.isNotEmpty()) keyActions.register(actionId, result.stringOrEmpty("binding").ifEmpty { null })
|
||||
}
|
||||
notifyIfSensitive(result)
|
||||
bridgeReplyProxy?.postMessage(result.toString())
|
||||
@@ -989,10 +988,10 @@ class NappletHostActivity : ComponentActivity() {
|
||||
* Surfaces an "allow always" capability acting on the user's behalf, so a granted RELAY/UPLOAD/VALUE
|
||||
* op can never run completely silently. Read-only ops (identity/storage/resource) don't toast.
|
||||
*/
|
||||
private fun notifyIfSensitive(result: JSONObject) {
|
||||
if (!result.optBoolean("ok")) return
|
||||
private fun notifyIfSensitive(result: JsonObject) {
|
||||
if (!result.booleanOrFalse("ok")) return
|
||||
val message =
|
||||
when (result.optString("type")) {
|
||||
when (result.stringOrEmpty("type")) {
|
||||
"relay.publish.result", "relay.publishEncrypted.result" -> getString(R.string.napplet_action_published, barTitle())
|
||||
"upload.upload.result" -> getString(R.string.napplet_action_uploaded, barTitle())
|
||||
"value.payInvoice.result" -> getString(R.string.napplet_action_paid, barTitle())
|
||||
|
||||
+8
-9
@@ -62,7 +62,7 @@ import com.vitorpamplona.quartz.nip01Core.core.toHexKey
|
||||
import com.vitorpamplona.quartz.nip5aStaticWebsites.tags.PathTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.sha256.sha256
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
@@ -583,10 +583,10 @@ class NappletHostService : Service() {
|
||||
tab.bridgeReplyProxy = replyProxy
|
||||
|
||||
val raw = message.data ?: return
|
||||
val envelope = runCatching { JSONObject(raw) }.getOrNull() ?: return
|
||||
val envelope = parseJsonObject(raw) ?: return
|
||||
|
||||
// IME events aren't brokered — the main app hosts the keyboard. Relay the envelope to the client.
|
||||
if (envelope.optString("type").startsWith("ime.")) {
|
||||
if (envelope.stringOrEmpty("type").startsWith("ime.")) {
|
||||
val reply =
|
||||
Message.obtain(null, NappletEmbedContract.MSG_IME_EVENT).apply {
|
||||
data = Bundle().apply { putString(NappletEmbedContract.KEY_IME_PAYLOAD, raw) }
|
||||
@@ -595,7 +595,7 @@ class NappletHostService : Service() {
|
||||
return
|
||||
}
|
||||
|
||||
val id = envelope.optString("id").ifEmpty { "fire-${tab.fireSeq++}" }
|
||||
val id = envelope.stringOrEmpty("id").ifEmpty { "fire-${tab.fireSeq++}" }
|
||||
val msg =
|
||||
Message.obtain(null, NappletIpc.MSG_REQUEST).apply {
|
||||
replyTo = tab.replyMessenger
|
||||
@@ -630,8 +630,7 @@ class NappletHostService : Service() {
|
||||
NappletIpc.MSG_RESPONSE -> {
|
||||
val id = data.getString(NappletIpc.KEY_REQUEST_ID) ?: return true
|
||||
val payload = data.getString(NappletIpc.KEY_PAYLOAD) ?: return true
|
||||
val result = runCatching { JSONObject(payload) }.getOrNull() ?: JSONObject()
|
||||
result.put("id", id)
|
||||
val result = (parseJsonObject(payload) ?: JsonObject(emptyMap())).withString("id", id)
|
||||
notifyIfSensitive(tab, result)
|
||||
runCatching { tab.bridgeReplyProxy?.postMessage(result.toString()) }
|
||||
}
|
||||
@@ -647,11 +646,11 @@ class NappletHostService : Service() {
|
||||
/** Pushes a notice to the main process for a granted "allow always" sensitive op, so it can toast. */
|
||||
private fun notifyIfSensitive(
|
||||
tab: NappletTab,
|
||||
result: JSONObject,
|
||||
result: JsonObject,
|
||||
) {
|
||||
if (!result.optBoolean("ok")) return
|
||||
if (!result.booleanOrFalse("ok")) return
|
||||
val notice =
|
||||
when (result.optString("type")) {
|
||||
when (result.stringOrEmpty("type")) {
|
||||
"relay.publish.result", "relay.publishEncrypted.result" -> NappletEmbedContract.NOTICE_PUBLISHED
|
||||
"upload.upload.result" -> NappletEmbedContract.NOTICE_UPLOADED
|
||||
"value.payInvoice.result" -> NappletEmbedContract.NOTICE_PAID
|
||||
|
||||
@@ -73,12 +73,11 @@ node shim-events.mjs /path/to/other/shim.js # diff a candidate against it
|
||||
Set `CHROMIUM_PATH` if your Chromium lives somewhere other than
|
||||
`/opt/pw-browsers/chromium-1194/chrome-linux/chrome`.
|
||||
|
||||
**Why this and not a JVM unit test.** The host-side parser
|
||||
(`parseImeEvent`) runs on Android's `org.json`, which the unit tests stub out
|
||||
(`unitTests.isReturnDefaultValues = true` in `amethyst/build.gradle.kts`, and
|
||||
there is no Robolectric); a Kotlin test would "pass" without parsing anything.
|
||||
The half worth protecting is the page↔host contract, and that only exists in a
|
||||
browser.
|
||||
**Why this and not a JVM unit test.** The half worth protecting is the
|
||||
page↔host contract — real browser focus/gesture behavior and the envelopes the
|
||||
shim emits for it — and that only exists in a browser. A JVM test of the
|
||||
host-side parser (`parseImeEvent`, kotlinx.serialization) would only re-parse
|
||||
envelopes the test itself fabricated.
|
||||
|
||||
## `perf.html` — why does the embed feel slower than the full-screen browser?
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
//
|
||||
// Loads the REAL shim (commons/src/commonMain/composeResources/files/napplet/shim.js) into real Chromium
|
||||
// with the embedded-surface flags set, drives genuine focus/tap/blur gestures, and asserts the `ime.*`
|
||||
// envelopes it emits. This is the only honest automated coverage for this code: the host-side parser runs
|
||||
// on Android's `org.json`, which the JVM unit tests stub out (`unitTests.isReturnDefaultValues = true`), so
|
||||
// a Kotlin test of it would pass without parsing anything.
|
||||
// envelopes it emits. This is the only honest automated coverage for this code: the half worth protecting
|
||||
// is the page↔host contract (real browser focus/gesture behavior), which no JVM unit test of the host-side
|
||||
// parser (`parseImeEvent`, kotlinx.serialization) can exercise.
|
||||
//
|
||||
// cd tools/ime-test && npm i playwright-core && node shim-events.mjs
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user