diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt index 7fec59073..b70a55edb 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt @@ -16,6 +16,10 @@ import com.steipete.clawdis.node.bridge.BridgePairingClient import com.steipete.clawdis.node.bridge.BridgeSession import com.steipete.clawdis.node.node.CameraCaptureManager 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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -264,15 +268,17 @@ class NodeRuntime(context: Context) { .ifEmpty { null } val resolved = if (storedToken.isNullOrBlank()) { - _statusText.value = "Pairing…" - val caps = buildList { - add("canvas") - if (cameraEnabled.value) add("camera") - if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) add("voiceWake") - } - BridgePairingClient().pairAndHello( - endpoint = endpoint, - hello = + _statusText.value = "Pairing…" + val caps = buildList { + add(ClawdisCapability.Canvas.rawValue) + if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue) + if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) { + add(ClawdisCapability.VoiceWake.rawValue) + } + } + BridgePairingClient().pairAndHello( + endpoint = endpoint, + hello = BridgePairingClient.Hello( nodeId = instanceId.value, displayName = displayName.value, @@ -305,17 +311,19 @@ class NodeRuntime(context: Context) { platform = "Android", version = "dev", deviceFamily = "Android", - modelIdentifier = modelIdentifier, - caps = - buildList { - add("canvas") - if (cameraEnabled.value) add("camera") - if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) add("voiceWake") - }, - ), - ) - } - } + modelIdentifier = modelIdentifier, + caps = + buildList { + add(ClawdisCapability.Canvas.rawValue) + if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue) + if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) { + add(ClawdisCapability.VoiceWake.rawValue) + } + }, + ), + ) + } + } private fun hasRecordAudioPermission(): Boolean { return ( @@ -422,7 +430,13 @@ class NodeRuntime(context: Context) { } 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) { return BridgeSession.InvokeResult.error( 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( code = "CAMERA_DISABLED", message = "CAMERA_DISABLED: enable Camera in Settings", ) } - return when (command) { - "canvas.show" -> BridgeSession.InvokeResult.ok(null) - "canvas.hide" -> BridgeSession.InvokeResult.ok(null) - "canvas.setMode" -> { + return when (canonicalCommand) { + ClawdisCanvasCommand.Show.rawValue -> BridgeSession.InvokeResult.ok(null) + ClawdisCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null) + ClawdisCanvasCommand.SetMode.rawValue -> { val mode = CanvasController.parseMode(paramsJson) canvas.setMode(mode) BridgeSession.InvokeResult.ok(null) } - "canvas.navigate" -> { + ClawdisCanvasCommand.Navigate.rawValue -> { val url = CanvasController.parseNavigateUrl(paramsJson) if (url != null) canvas.navigate(url) BridgeSession.InvokeResult.ok(null) } - "canvas.eval" -> { + ClawdisCanvasCommand.Eval.rawValue -> { val js = CanvasController.parseEvalJs(paramsJson) ?: return BridgeSession.InvokeResult.error( @@ -468,7 +482,7 @@ class NodeRuntime(context: Context) { } BridgeSession.InvokeResult.ok("""{"result":${result.toJsonString()}}""") } - "canvas.snapshot" -> { + ClawdisCanvasCommand.Snapshot.rawValue -> { val maxWidth = CanvasController.parseSnapshotMaxWidth(paramsJson) val base64 = try { @@ -481,11 +495,11 @@ class NodeRuntime(context: Context) { } BridgeSession.InvokeResult.ok("""{"format":"png","base64":"$base64"}""") } - "camera.snap" -> { + ClawdisCameraCommand.Snap.rawValue -> { val res = camera.snap(paramsJson) BridgeSession.InvokeResult.ok(res.payloadJson) } - "camera.clip" -> { + ClawdisCameraCommand.Clip.rawValue -> { val includeAudio = paramsJson?.contains("\"includeAudio\":true") != false if (includeAudio) externalAudioCaptureActive.value = true try { diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstants.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstants.kt new file mode 100644 index 000000000..1525bf264 --- /dev/null +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstants.kt @@ -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 + } +} diff --git a/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstantsTest.kt b/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstantsTest.kt new file mode 100644 index 000000000..aaa6a6fdb --- /dev/null +++ b/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstantsTest.kt @@ -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) + } +}