Android: centralize canvas protocol strings
This commit is contained in:
@@ -16,6 +16,10 @@ import com.steipete.clawdis.node.bridge.BridgePairingClient
|
|||||||
import com.steipete.clawdis.node.bridge.BridgeSession
|
import com.steipete.clawdis.node.bridge.BridgeSession
|
||||||
import com.steipete.clawdis.node.node.CameraCaptureManager
|
import com.steipete.clawdis.node.node.CameraCaptureManager
|
||||||
import com.steipete.clawdis.node.node.CanvasController
|
import com.steipete.clawdis.node.node.CanvasController
|
||||||
|
import com.steipete.clawdis.node.protocol.ClawdisCapability
|
||||||
|
import com.steipete.clawdis.node.protocol.ClawdisCameraCommand
|
||||||
|
import com.steipete.clawdis.node.protocol.ClawdisCanvasCommand
|
||||||
|
import com.steipete.clawdis.node.protocol.ClawdisInvokeCommandAliases
|
||||||
import com.steipete.clawdis.node.voice.VoiceWakeManager
|
import com.steipete.clawdis.node.voice.VoiceWakeManager
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -264,15 +268,17 @@ class NodeRuntime(context: Context) {
|
|||||||
.ifEmpty { null }
|
.ifEmpty { null }
|
||||||
val resolved =
|
val resolved =
|
||||||
if (storedToken.isNullOrBlank()) {
|
if (storedToken.isNullOrBlank()) {
|
||||||
_statusText.value = "Pairing…"
|
_statusText.value = "Pairing…"
|
||||||
val caps = buildList {
|
val caps = buildList {
|
||||||
add("canvas")
|
add(ClawdisCapability.Canvas.rawValue)
|
||||||
if (cameraEnabled.value) add("camera")
|
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
|
||||||
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) add("voiceWake")
|
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
|
||||||
}
|
add(ClawdisCapability.VoiceWake.rawValue)
|
||||||
BridgePairingClient().pairAndHello(
|
}
|
||||||
endpoint = endpoint,
|
}
|
||||||
hello =
|
BridgePairingClient().pairAndHello(
|
||||||
|
endpoint = endpoint,
|
||||||
|
hello =
|
||||||
BridgePairingClient.Hello(
|
BridgePairingClient.Hello(
|
||||||
nodeId = instanceId.value,
|
nodeId = instanceId.value,
|
||||||
displayName = displayName.value,
|
displayName = displayName.value,
|
||||||
@@ -305,17 +311,19 @@ class NodeRuntime(context: Context) {
|
|||||||
platform = "Android",
|
platform = "Android",
|
||||||
version = "dev",
|
version = "dev",
|
||||||
deviceFamily = "Android",
|
deviceFamily = "Android",
|
||||||
modelIdentifier = modelIdentifier,
|
modelIdentifier = modelIdentifier,
|
||||||
caps =
|
caps =
|
||||||
buildList {
|
buildList {
|
||||||
add("canvas")
|
add(ClawdisCapability.Canvas.rawValue)
|
||||||
if (cameraEnabled.value) add("camera")
|
if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue)
|
||||||
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) add("voiceWake")
|
if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) {
|
||||||
},
|
add(ClawdisCapability.VoiceWake.rawValue)
|
||||||
),
|
}
|
||||||
)
|
},
|
||||||
}
|
),
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun hasRecordAudioPermission(): Boolean {
|
private fun hasRecordAudioPermission(): Boolean {
|
||||||
return (
|
return (
|
||||||
@@ -422,7 +430,13 @@ class NodeRuntime(context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun handleInvoke(command: String, paramsJson: String?): BridgeSession.InvokeResult {
|
private suspend fun handleInvoke(command: String, paramsJson: String?): BridgeSession.InvokeResult {
|
||||||
if (command.startsWith("canvas.") || command.startsWith("camera.")) {
|
// Back-compat: accept screen.* commands and map them to canvas.*.
|
||||||
|
val canonicalCommand = ClawdisInvokeCommandAliases.canonicalizeScreenToCanvas(command)
|
||||||
|
|
||||||
|
if (
|
||||||
|
canonicalCommand.startsWith(ClawdisCanvasCommand.NamespacePrefix) ||
|
||||||
|
canonicalCommand.startsWith(ClawdisCameraCommand.NamespacePrefix)
|
||||||
|
) {
|
||||||
if (!isForeground.value) {
|
if (!isForeground.value) {
|
||||||
return BridgeSession.InvokeResult.error(
|
return BridgeSession.InvokeResult.error(
|
||||||
code = "NODE_BACKGROUND_UNAVAILABLE",
|
code = "NODE_BACKGROUND_UNAVAILABLE",
|
||||||
@@ -430,27 +444,27 @@ class NodeRuntime(context: Context) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (command.startsWith("camera.") && !cameraEnabled.value) {
|
if (canonicalCommand.startsWith(ClawdisCameraCommand.NamespacePrefix) && !cameraEnabled.value) {
|
||||||
return BridgeSession.InvokeResult.error(
|
return BridgeSession.InvokeResult.error(
|
||||||
code = "CAMERA_DISABLED",
|
code = "CAMERA_DISABLED",
|
||||||
message = "CAMERA_DISABLED: enable Camera in Settings",
|
message = "CAMERA_DISABLED: enable Camera in Settings",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (command) {
|
return when (canonicalCommand) {
|
||||||
"canvas.show" -> BridgeSession.InvokeResult.ok(null)
|
ClawdisCanvasCommand.Show.rawValue -> BridgeSession.InvokeResult.ok(null)
|
||||||
"canvas.hide" -> BridgeSession.InvokeResult.ok(null)
|
ClawdisCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null)
|
||||||
"canvas.setMode" -> {
|
ClawdisCanvasCommand.SetMode.rawValue -> {
|
||||||
val mode = CanvasController.parseMode(paramsJson)
|
val mode = CanvasController.parseMode(paramsJson)
|
||||||
canvas.setMode(mode)
|
canvas.setMode(mode)
|
||||||
BridgeSession.InvokeResult.ok(null)
|
BridgeSession.InvokeResult.ok(null)
|
||||||
}
|
}
|
||||||
"canvas.navigate" -> {
|
ClawdisCanvasCommand.Navigate.rawValue -> {
|
||||||
val url = CanvasController.parseNavigateUrl(paramsJson)
|
val url = CanvasController.parseNavigateUrl(paramsJson)
|
||||||
if (url != null) canvas.navigate(url)
|
if (url != null) canvas.navigate(url)
|
||||||
BridgeSession.InvokeResult.ok(null)
|
BridgeSession.InvokeResult.ok(null)
|
||||||
}
|
}
|
||||||
"canvas.eval" -> {
|
ClawdisCanvasCommand.Eval.rawValue -> {
|
||||||
val js =
|
val js =
|
||||||
CanvasController.parseEvalJs(paramsJson)
|
CanvasController.parseEvalJs(paramsJson)
|
||||||
?: return BridgeSession.InvokeResult.error(
|
?: return BridgeSession.InvokeResult.error(
|
||||||
@@ -468,7 +482,7 @@ class NodeRuntime(context: Context) {
|
|||||||
}
|
}
|
||||||
BridgeSession.InvokeResult.ok("""{"result":${result.toJsonString()}}""")
|
BridgeSession.InvokeResult.ok("""{"result":${result.toJsonString()}}""")
|
||||||
}
|
}
|
||||||
"canvas.snapshot" -> {
|
ClawdisCanvasCommand.Snapshot.rawValue -> {
|
||||||
val maxWidth = CanvasController.parseSnapshotMaxWidth(paramsJson)
|
val maxWidth = CanvasController.parseSnapshotMaxWidth(paramsJson)
|
||||||
val base64 =
|
val base64 =
|
||||||
try {
|
try {
|
||||||
@@ -481,11 +495,11 @@ class NodeRuntime(context: Context) {
|
|||||||
}
|
}
|
||||||
BridgeSession.InvokeResult.ok("""{"format":"png","base64":"$base64"}""")
|
BridgeSession.InvokeResult.ok("""{"format":"png","base64":"$base64"}""")
|
||||||
}
|
}
|
||||||
"camera.snap" -> {
|
ClawdisCameraCommand.Snap.rawValue -> {
|
||||||
val res = camera.snap(paramsJson)
|
val res = camera.snap(paramsJson)
|
||||||
BridgeSession.InvokeResult.ok(res.payloadJson)
|
BridgeSession.InvokeResult.ok(res.payloadJson)
|
||||||
}
|
}
|
||||||
"camera.clip" -> {
|
ClawdisCameraCommand.Clip.rawValue -> {
|
||||||
val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false
|
val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false
|
||||||
if (includeAudio) externalAudioCaptureActive.value = true
|
if (includeAudio) externalAudioCaptureActive.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package com.steipete.clawdis.node.protocol
|
||||||
|
|
||||||
|
enum class ClawdisCapability(val rawValue: String) {
|
||||||
|
Canvas("canvas"),
|
||||||
|
Camera("camera"),
|
||||||
|
VoiceWake("voiceWake"),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ClawdisScreenCommand(val rawValue: String) {
|
||||||
|
Show("screen.show"),
|
||||||
|
Hide("screen.hide"),
|
||||||
|
SetMode("screen.setMode"),
|
||||||
|
Navigate("screen.navigate"),
|
||||||
|
Eval("screen.eval"),
|
||||||
|
Snapshot("screen.snapshot"),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val NamespacePrefix: String = "screen."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ClawdisCanvasCommand(val rawValue: String) {
|
||||||
|
Show("canvas.show"),
|
||||||
|
Hide("canvas.hide"),
|
||||||
|
SetMode("canvas.setMode"),
|
||||||
|
Navigate("canvas.navigate"),
|
||||||
|
Eval("canvas.eval"),
|
||||||
|
Snapshot("canvas.snapshot"),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val NamespacePrefix: String = "canvas."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ClawdisCameraCommand(val rawValue: String) {
|
||||||
|
Snap("camera.snap"),
|
||||||
|
Clip("camera.clip"),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val NamespacePrefix: String = "camera."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ClawdisInvokeCommandAliases {
|
||||||
|
fun canonicalizeScreenToCanvas(command: String): String {
|
||||||
|
if (command.startsWith(ClawdisScreenCommand.NamespacePrefix)) {
|
||||||
|
return ClawdisCanvasCommand.NamespacePrefix +
|
||||||
|
command.removePrefix(ClawdisScreenCommand.NamespacePrefix)
|
||||||
|
}
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.steipete.clawdis.node.protocol
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ClawdisProtocolConstantsTest {
|
||||||
|
@Test
|
||||||
|
fun mapsKnownScreenCommandsToCanvas() {
|
||||||
|
val mappings =
|
||||||
|
listOf(
|
||||||
|
Pair(ClawdisScreenCommand.Show, ClawdisCanvasCommand.Show),
|
||||||
|
Pair(ClawdisScreenCommand.Hide, ClawdisCanvasCommand.Hide),
|
||||||
|
Pair(ClawdisScreenCommand.SetMode, ClawdisCanvasCommand.SetMode),
|
||||||
|
Pair(ClawdisScreenCommand.Navigate, ClawdisCanvasCommand.Navigate),
|
||||||
|
Pair(ClawdisScreenCommand.Eval, ClawdisCanvasCommand.Eval),
|
||||||
|
Pair(ClawdisScreenCommand.Snapshot, ClawdisCanvasCommand.Snapshot),
|
||||||
|
)
|
||||||
|
|
||||||
|
for ((screen, canvas) in mappings) {
|
||||||
|
assertEquals(
|
||||||
|
canvas.rawValue,
|
||||||
|
ClawdisInvokeCommandAliases.canonicalizeScreenToCanvas(screen.rawValue),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun mapsUnknownScreenNamespaceToCanvas() {
|
||||||
|
assertEquals("canvas.foo", ClawdisInvokeCommandAliases.canonicalizeScreenToCanvas("screen.foo"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun leavesNonScreenCommandsUnchanged() {
|
||||||
|
assertEquals(
|
||||||
|
ClawdisCameraCommand.Snap.rawValue,
|
||||||
|
ClawdisInvokeCommandAliases.canonicalizeScreenToCanvas(ClawdisCameraCommand.Snap.rawValue),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun capabilitiesUseStableStrings() {
|
||||||
|
assertEquals("canvas", ClawdisCapability.Canvas.rawValue)
|
||||||
|
assertEquals("camera", ClawdisCapability.Camera.rawValue)
|
||||||
|
assertEquals("voiceWake", ClawdisCapability.VoiceWake.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user