diff --git a/apps/android/app/src/main/java/ai/openclaw/android/node/ConnectionManager.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/ConnectionManager.kt index 1c9a045c896..61cc63802e1 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/node/ConnectionManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/ConnectionManager.kt @@ -7,7 +7,6 @@ import ai.openclaw.android.gateway.GatewayClientInfo import ai.openclaw.android.gateway.GatewayConnectOptions import ai.openclaw.android.gateway.GatewayEndpoint import ai.openclaw.android.gateway.GatewayTlsParams -import ai.openclaw.android.protocol.OpenClawCapability import ai.openclaw.android.LocationMode import ai.openclaw.android.VoiceWakeMode @@ -73,28 +72,18 @@ class ConnectionManager( } } - fun buildInvokeCommands(): List = - InvokeCommandRegistry.advertisedCommands( + private fun runtimeFlags(): NodeRuntimeFlags = + NodeRuntimeFlags( cameraEnabled = cameraEnabled(), locationEnabled = locationMode() != LocationMode.Off, smsAvailable = smsAvailable(), + voiceWakeEnabled = voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission(), debugBuild = BuildConfig.DEBUG, ) - fun buildCapabilities(): List = - buildList { - add(OpenClawCapability.Canvas.rawValue) - add(OpenClawCapability.Screen.rawValue) - add(OpenClawCapability.Device.rawValue) - if (cameraEnabled()) add(OpenClawCapability.Camera.rawValue) - if (smsAvailable()) add(OpenClawCapability.Sms.rawValue) - if (voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission()) { - add(OpenClawCapability.VoiceWake.rawValue) - } - if (locationMode() != LocationMode.Off) { - add(OpenClawCapability.Location.rawValue) - } - } + fun buildInvokeCommands(): List = InvokeCommandRegistry.advertisedCommands(runtimeFlags()) + + fun buildCapabilities(): List = InvokeCommandRegistry.advertisedCapabilities(runtimeFlags()) fun resolvedVersionName(): String { val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" } diff --git a/apps/android/app/src/main/java/ai/openclaw/android/node/InvokeCommandRegistry.kt b/apps/android/app/src/main/java/ai/openclaw/android/node/InvokeCommandRegistry.kt index 89af1b159fa..757db106475 100644 --- a/apps/android/app/src/main/java/ai/openclaw/android/node/InvokeCommandRegistry.kt +++ b/apps/android/app/src/main/java/ai/openclaw/android/node/InvokeCommandRegistry.kt @@ -3,12 +3,21 @@ package ai.openclaw.android.node import ai.openclaw.android.protocol.OpenClawCanvasA2UICommand import ai.openclaw.android.protocol.OpenClawCanvasCommand import ai.openclaw.android.protocol.OpenClawCameraCommand +import ai.openclaw.android.protocol.OpenClawCapability import ai.openclaw.android.protocol.OpenClawDeviceCommand import ai.openclaw.android.protocol.OpenClawLocationCommand import ai.openclaw.android.protocol.OpenClawNotificationsCommand import ai.openclaw.android.protocol.OpenClawScreenCommand import ai.openclaw.android.protocol.OpenClawSmsCommand +data class NodeRuntimeFlags( + val cameraEnabled: Boolean, + val locationEnabled: Boolean, + val smsAvailable: Boolean, + val voiceWakeEnabled: Boolean, + val debugBuild: Boolean, +) + enum class InvokeCommandAvailability { Always, CameraEnabled, @@ -17,6 +26,19 @@ enum class InvokeCommandAvailability { DebugBuild, } +enum class NodeCapabilityAvailability { + Always, + CameraEnabled, + LocationEnabled, + SmsAvailable, + VoiceWakeEnabled, +} + +data class NodeCapabilitySpec( + val name: String, + val availability: NodeCapabilityAvailability = NodeCapabilityAvailability.Always, +) + data class InvokeCommandSpec( val name: String, val requiresForeground: Boolean = false, @@ -24,6 +46,29 @@ data class InvokeCommandSpec( ) object InvokeCommandRegistry { + val capabilityManifest: List = + listOf( + NodeCapabilitySpec(name = OpenClawCapability.Canvas.rawValue), + NodeCapabilitySpec(name = OpenClawCapability.Screen.rawValue), + NodeCapabilitySpec(name = OpenClawCapability.Device.rawValue), + NodeCapabilitySpec( + name = OpenClawCapability.Camera.rawValue, + availability = NodeCapabilityAvailability.CameraEnabled, + ), + NodeCapabilitySpec( + name = OpenClawCapability.Sms.rawValue, + availability = NodeCapabilityAvailability.SmsAvailable, + ), + NodeCapabilitySpec( + name = OpenClawCapability.VoiceWake.rawValue, + availability = NodeCapabilityAvailability.VoiceWakeEnabled, + ), + NodeCapabilitySpec( + name = OpenClawCapability.Location.rawValue, + availability = NodeCapabilityAvailability.LocationEnabled, + ), + ) + val all: List = listOf( InvokeCommandSpec( @@ -118,20 +163,29 @@ object InvokeCommandRegistry { fun find(command: String): InvokeCommandSpec? = byNameInternal[command] - fun advertisedCommands( - cameraEnabled: Boolean, - locationEnabled: Boolean, - smsAvailable: Boolean, - debugBuild: Boolean, - ): List { + fun advertisedCapabilities(flags: NodeRuntimeFlags): List { + return capabilityManifest + .filter { spec -> + when (spec.availability) { + NodeCapabilityAvailability.Always -> true + NodeCapabilityAvailability.CameraEnabled -> flags.cameraEnabled + NodeCapabilityAvailability.LocationEnabled -> flags.locationEnabled + NodeCapabilityAvailability.SmsAvailable -> flags.smsAvailable + NodeCapabilityAvailability.VoiceWakeEnabled -> flags.voiceWakeEnabled + } + } + .map { it.name } + } + + fun advertisedCommands(flags: NodeRuntimeFlags): List { return all .filter { spec -> when (spec.availability) { InvokeCommandAvailability.Always -> true - InvokeCommandAvailability.CameraEnabled -> cameraEnabled - InvokeCommandAvailability.LocationEnabled -> locationEnabled - InvokeCommandAvailability.SmsAvailable -> smsAvailable - InvokeCommandAvailability.DebugBuild -> debugBuild + InvokeCommandAvailability.CameraEnabled -> flags.cameraEnabled + InvokeCommandAvailability.LocationEnabled -> flags.locationEnabled + InvokeCommandAvailability.SmsAvailable -> flags.smsAvailable + InvokeCommandAvailability.DebugBuild -> flags.debugBuild } } .map { it.name } diff --git a/apps/android/app/src/test/java/ai/openclaw/android/node/InvokeCommandRegistryTest.kt b/apps/android/app/src/test/java/ai/openclaw/android/node/InvokeCommandRegistryTest.kt index db72f0f842a..39b47190e24 100644 --- a/apps/android/app/src/test/java/ai/openclaw/android/node/InvokeCommandRegistryTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/android/node/InvokeCommandRegistryTest.kt @@ -1,6 +1,7 @@ package ai.openclaw.android.node import ai.openclaw.android.protocol.OpenClawCameraCommand +import ai.openclaw.android.protocol.OpenClawCapability import ai.openclaw.android.protocol.OpenClawDeviceCommand import ai.openclaw.android.protocol.OpenClawLocationCommand import ai.openclaw.android.protocol.OpenClawNotificationsCommand @@ -10,14 +11,61 @@ import org.junit.Assert.assertTrue import org.junit.Test class InvokeCommandRegistryTest { + @Test + fun advertisedCapabilities_respectsFeatureAvailability() { + val capabilities = + InvokeCommandRegistry.advertisedCapabilities( + NodeRuntimeFlags( + cameraEnabled = false, + locationEnabled = false, + smsAvailable = false, + voiceWakeEnabled = false, + debugBuild = false, + ), + ) + + assertTrue(capabilities.contains(OpenClawCapability.Canvas.rawValue)) + assertTrue(capabilities.contains(OpenClawCapability.Screen.rawValue)) + assertTrue(capabilities.contains(OpenClawCapability.Device.rawValue)) + assertFalse(capabilities.contains(OpenClawCapability.Camera.rawValue)) + assertFalse(capabilities.contains(OpenClawCapability.Location.rawValue)) + assertFalse(capabilities.contains(OpenClawCapability.Sms.rawValue)) + assertFalse(capabilities.contains(OpenClawCapability.VoiceWake.rawValue)) + } + + @Test + fun advertisedCapabilities_includesFeatureCapabilitiesWhenEnabled() { + val capabilities = + InvokeCommandRegistry.advertisedCapabilities( + NodeRuntimeFlags( + cameraEnabled = true, + locationEnabled = true, + smsAvailable = true, + voiceWakeEnabled = true, + debugBuild = false, + ), + ) + + assertTrue(capabilities.contains(OpenClawCapability.Canvas.rawValue)) + assertTrue(capabilities.contains(OpenClawCapability.Screen.rawValue)) + assertTrue(capabilities.contains(OpenClawCapability.Device.rawValue)) + assertTrue(capabilities.contains(OpenClawCapability.Camera.rawValue)) + assertTrue(capabilities.contains(OpenClawCapability.Location.rawValue)) + assertTrue(capabilities.contains(OpenClawCapability.Sms.rawValue)) + assertTrue(capabilities.contains(OpenClawCapability.VoiceWake.rawValue)) + } + @Test fun advertisedCommands_respectsFeatureAvailability() { val commands = InvokeCommandRegistry.advertisedCommands( - cameraEnabled = false, - locationEnabled = false, - smsAvailable = false, - debugBuild = false, + NodeRuntimeFlags( + cameraEnabled = false, + locationEnabled = false, + smsAvailable = false, + voiceWakeEnabled = false, + debugBuild = false, + ), ) assertFalse(commands.contains(OpenClawCameraCommand.Snap.rawValue)) @@ -40,10 +88,13 @@ class InvokeCommandRegistryTest { fun advertisedCommands_includesFeatureCommandsWhenEnabled() { val commands = InvokeCommandRegistry.advertisedCommands( - cameraEnabled = true, - locationEnabled = true, - smsAvailable = true, - debugBuild = true, + NodeRuntimeFlags( + cameraEnabled = true, + locationEnabled = true, + smsAvailable = true, + voiceWakeEnabled = false, + debugBuild = true, + ), ) assertTrue(commands.contains(OpenClawCameraCommand.Snap.rawValue))