diff --git a/apps/android/app/src/main/java/com/clawdis/android/MainViewModel.kt b/apps/android/app/src/main/java/com/clawdis/android/MainViewModel.kt index d943cdccd..54ec07ffe 100644 --- a/apps/android/app/src/main/java/com/clawdis/android/MainViewModel.kt +++ b/apps/android/app/src/main/java/com/clawdis/android/MainViewModel.kt @@ -118,6 +118,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { runtime.setTalkEnabled(enabled) } + fun refreshBridgeHello() { + runtime.refreshBridgeHello() + } + fun connect(endpoint: BridgeEndpoint) { runtime.connect(endpoint) } diff --git a/apps/android/app/src/main/java/com/clawdis/android/NodeRuntime.kt b/apps/android/app/src/main/java/com/clawdis/android/NodeRuntime.kt index 18faffd49..203bf75b3 100644 --- a/apps/android/app/src/main/java/com/clawdis/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/clawdis/android/NodeRuntime.kt @@ -367,73 +367,112 @@ class NodeRuntime(context: Context) { prefs.setTalkEnabled(value) } + private fun buildInvokeCommands(): List = + buildList { + add(ClawdisCanvasCommand.Present.rawValue) + add(ClawdisCanvasCommand.Hide.rawValue) + add(ClawdisCanvasCommand.Navigate.rawValue) + add(ClawdisCanvasCommand.Eval.rawValue) + add(ClawdisCanvasCommand.Snapshot.rawValue) + add(ClawdisCanvasA2UICommand.Push.rawValue) + add(ClawdisCanvasA2UICommand.PushJSONL.rawValue) + add(ClawdisCanvasA2UICommand.Reset.rawValue) + add(ClawdisScreenCommand.Record.rawValue) + if (cameraEnabled.value) { + add(ClawdisCameraCommand.Snap.rawValue) + add(ClawdisCameraCommand.Clip.rawValue) + } + if (locationMode.value != LocationMode.Off) { + add(ClawdisLocationCommand.Get.rawValue) + } + if (sms.canSendSms()) { + add(ClawdisSmsCommand.Send.rawValue) + } + } + + private fun buildCapabilities(): List = + buildList { + add(ClawdisCapability.Canvas.rawValue) + add(ClawdisCapability.Screen.rawValue) + if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue) + if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue) + if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) { + add(ClawdisCapability.VoiceWake.rawValue) + } + if (locationMode.value != LocationMode.Off) { + add(ClawdisCapability.Location.rawValue) + } + } + + private fun buildPairingHello(token: String?): BridgePairingClient.Hello { + val modelIdentifier = listOfNotNull(Build.MANUFACTURER, Build.MODEL) + .joinToString(" ") + .trim() + .ifEmpty { null } + val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" } + val advertisedVersion = + if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) { + "$versionName-dev" + } else { + versionName + } + return BridgePairingClient.Hello( + nodeId = instanceId.value, + displayName = displayName.value, + token = token, + platform = "Android", + version = advertisedVersion, + deviceFamily = "Android", + modelIdentifier = modelIdentifier, + caps = buildCapabilities(), + commands = buildInvokeCommands(), + ) + } + + private fun buildSessionHello(token: String?): BridgeSession.Hello { + val modelIdentifier = listOfNotNull(Build.MANUFACTURER, Build.MODEL) + .joinToString(" ") + .trim() + .ifEmpty { null } + val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" } + val advertisedVersion = + if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) { + "$versionName-dev" + } else { + versionName + } + return BridgeSession.Hello( + nodeId = instanceId.value, + displayName = displayName.value, + token = token, + platform = "Android", + version = advertisedVersion, + deviceFamily = "Android", + modelIdentifier = modelIdentifier, + caps = buildCapabilities(), + commands = buildInvokeCommands(), + ) + } + + fun refreshBridgeHello() { + scope.launch { + if (!_isConnected.value) return@launch + val token = prefs.loadBridgeToken() + if (token.isNullOrBlank()) return@launch + session.updateHello(buildSessionHello(token)) + } + } + fun connect(endpoint: BridgeEndpoint) { scope.launch { _statusText.value = "Connecting…" val storedToken = prefs.loadBridgeToken() - val modelIdentifier = listOfNotNull(Build.MANUFACTURER, Build.MODEL) - .joinToString(" ") - .trim() - .ifEmpty { null } - - val invokeCommands = - buildList { - add(ClawdisCanvasCommand.Present.rawValue) - add(ClawdisCanvasCommand.Hide.rawValue) - add(ClawdisCanvasCommand.Navigate.rawValue) - add(ClawdisCanvasCommand.Eval.rawValue) - add(ClawdisCanvasCommand.Snapshot.rawValue) - add(ClawdisCanvasA2UICommand.Push.rawValue) - add(ClawdisCanvasA2UICommand.PushJSONL.rawValue) - add(ClawdisCanvasA2UICommand.Reset.rawValue) - add(ClawdisScreenCommand.Record.rawValue) - if (cameraEnabled.value) { - add(ClawdisCameraCommand.Snap.rawValue) - add(ClawdisCameraCommand.Clip.rawValue) - } - if (locationMode.value != LocationMode.Off) { - add(ClawdisLocationCommand.Get.rawValue) - } - if (sms.canSendSms()) { - add(ClawdisSmsCommand.Send.rawValue) - } - } val resolved = if (storedToken.isNullOrBlank()) { _statusText.value = "Pairing…" - val caps = buildList { - add(ClawdisCapability.Canvas.rawValue) - add(ClawdisCapability.Screen.rawValue) - if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue) - if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue) - if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) { - add(ClawdisCapability.VoiceWake.rawValue) - } - if (locationMode.value != LocationMode.Off) { - add(ClawdisCapability.Location.rawValue) - } - } - val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" } - val advertisedVersion = - if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) { - "$versionName-dev" - } else { - versionName - } BridgePairingClient().pairAndHello( endpoint = endpoint, - hello = - BridgePairingClient.Hello( - nodeId = instanceId.value, - displayName = displayName.value, - token = null, - platform = "Android", - version = advertisedVersion, - deviceFamily = "Android", - modelIdentifier = modelIdentifier, - caps = caps, - commands = invokeCommands, - ), + hello = buildPairingHello(token = null), ) } else { BridgePairingClient.PairResult(ok = true, token = storedToken.trim()) @@ -447,39 +486,9 @@ class NodeRuntime(context: Context) { val authToken = requireNotNull(resolved.token).trim() prefs.saveBridgeToken(authToken) - val versionName = BuildConfig.VERSION_NAME.trim().ifEmpty { "dev" } - val advertisedVersion = - if (BuildConfig.DEBUG && !versionName.contains("dev", ignoreCase = true)) { - "$versionName-dev" - } else { - versionName - } session.connect( endpoint = endpoint, - hello = - BridgeSession.Hello( - nodeId = instanceId.value, - displayName = displayName.value, - token = authToken, - platform = "Android", - version = advertisedVersion, - deviceFamily = "Android", - modelIdentifier = modelIdentifier, - caps = - buildList { - add(ClawdisCapability.Canvas.rawValue) - add(ClawdisCapability.Screen.rawValue) - if (cameraEnabled.value) add(ClawdisCapability.Camera.rawValue) - if (sms.canSendSms()) add(ClawdisCapability.Sms.rawValue) - if (voiceWakeMode.value != VoiceWakeMode.Off && hasRecordAudioPermission()) { - add(ClawdisCapability.VoiceWake.rawValue) - } - if (locationMode.value != LocationMode.Off) { - add(ClawdisCapability.Location.rawValue) - } - }, - commands = invokeCommands, - ), + hello = buildSessionHello(token = authToken), ) } } diff --git a/apps/android/app/src/main/java/com/clawdis/android/bridge/BridgeSession.kt b/apps/android/app/src/main/java/com/clawdis/android/bridge/BridgeSession.kt index 50346509e..0325a98e3 100644 --- a/apps/android/app/src/main/java/com/clawdis/android/bridge/BridgeSession.kt +++ b/apps/android/app/src/main/java/com/clawdis/android/bridge/BridgeSession.kt @@ -75,6 +75,13 @@ class BridgeSession( } } + suspend fun updateHello(hello: Hello) { + val target = desired ?: return + desired = target.first to hello + val conn = currentConnection ?: return + conn.sendJson(buildHelloJson(hello)) + } + fun disconnect() { desired = null // Unblock connectOnce() read loop. Coroutine cancellation alone won't interrupt BufferedReader.readLine(). @@ -196,20 +203,7 @@ class BridgeSession( currentConnection = conn try { - conn.sendJson( - buildJsonObject { - put("type", JsonPrimitive("hello")) - put("nodeId", JsonPrimitive(hello.nodeId)) - hello.displayName?.let { put("displayName", JsonPrimitive(it)) } - hello.token?.let { put("token", JsonPrimitive(it)) } - hello.platform?.let { put("platform", JsonPrimitive(it)) } - hello.version?.let { put("version", JsonPrimitive(it)) } - hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) } - hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) } - hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) } - hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) } - }, - ) + conn.sendJson(buildHelloJson(hello)) val firstLine = reader.readLine() ?: throw IllegalStateException("bridge closed connection") val first = json.parseToJsonElement(firstLine).asObjectOrNull() @@ -307,6 +301,20 @@ class BridgeSession( } } + private fun buildHelloJson(hello: Hello): JsonObject = + buildJsonObject { + put("type", JsonPrimitive("hello")) + put("nodeId", JsonPrimitive(hello.nodeId)) + hello.displayName?.let { put("displayName", JsonPrimitive(it)) } + hello.token?.let { put("token", JsonPrimitive(it)) } + hello.platform?.let { put("platform", JsonPrimitive(it)) } + hello.version?.let { put("version", JsonPrimitive(it)) } + hello.deviceFamily?.let { put("deviceFamily", JsonPrimitive(it)) } + hello.modelIdentifier?.let { put("modelIdentifier", JsonPrimitive(it)) } + hello.caps?.let { put("caps", JsonArray(it.map(::JsonPrimitive))) } + hello.commands?.let { put("commands", JsonArray(it.map(::JsonPrimitive))) } + } + private fun normalizeCanvasHostUrl(raw: String?, endpoint: BridgeEndpoint): String? { val trimmed = raw?.trim().orEmpty() val parsed = trimmed.takeIf { it.isNotBlank() }?.let { runCatching { URI(it) }.getOrNull() } diff --git a/apps/android/app/src/main/java/com/clawdis/android/ui/SettingsSheet.kt b/apps/android/app/src/main/java/com/clawdis/android/ui/SettingsSheet.kt index 85f492495..330c6c983 100644 --- a/apps/android/app/src/main/java/com/clawdis/android/ui/SettingsSheet.kt +++ b/apps/android/app/src/main/java/com/clawdis/android/ui/SettingsSheet.kt @@ -163,6 +163,7 @@ fun SettingsSheet(viewModel: MainViewModel) { val smsPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> smsPermissionGranted = granted + viewModel.refreshBridgeHello() } fun setCameraEnabledChecked(checked: Boolean) {