diff --git a/app/src/main/java/com/inou/clawdnode/gateway/DirectGateway.kt b/app/src/main/java/com/inou/clawdnode/gateway/DirectGateway.kt index c2a2eb9..06e4013 100644 --- a/app/src/main/java/com/inou/clawdnode/gateway/DirectGateway.kt +++ b/app/src/main/java/com/inou/clawdnode/gateway/DirectGateway.kt @@ -9,14 +9,28 @@ import org.json.JSONObject import java.util.concurrent.TimeUnit /** - * Direct WebSocket connection to our own ClawdNode Gateway. - * No authentication, no restrictions - full bidirectional control. + * Direct WebSocket connection to ClawdNode Gateway. + * Uses ClawdNode protocol (not Clawdbot protocol). + * + * Protocol: + * 1. Server sends: {"type":"welcome","protocol":"clawdnode/1.0",...} + * 2. Client sends: {"type":"hello","client":"clawdnode-android","version":"..."} + * 3. Server sends: {"type":"ready","message":"Connected"} + * 4. Bidirectional events/commands */ object DirectGateway { private const val TAG = "DirectGateway" + private const val PROTOCOL_VERSION = "clawdnode/1.0" - // Our gateway - Tailscale IP of james server - private const val GATEWAY_URL = "ws://100.123.216.65:9878" + // Default gateway URL (can be overridden via TokenStore) + private const val DEFAULT_GATEWAY_URL = "ws://100.123.216.65:9878" + + // Get URL from TokenStore or use default + private val gatewayUrl: String + get() = ClawdNodeApp.instance.tokenStore.gatewayUrl?.let { url -> + // Ensure it's a WebSocket URL + url.replace("http://", "ws://").replace("https://", "wss://").trimEnd('/') + } ?: DEFAULT_GATEWAY_URL private val client = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) @@ -34,31 +48,34 @@ object DirectGateway { var onCallReject: ((callId: String) -> Unit)? = null var onCallHangup: ((callId: String) -> Unit)? = null + // Connection state callback + var onConnectionChange: ((Boolean) -> Unit)? = null + var onLog: ((String) -> Unit)? = null + + private fun log(message: String) { + Log.d(TAG, message) + onLog?.invoke(message) + } + fun connect() { if (webSocket != null) { - Log.d(TAG, "Already connected or connecting") + log("Already connected or connecting") return } - Log.i(TAG, "Connecting to $GATEWAY_URL") - DebugClient.lifecycle("DIRECT_GATEWAY", "Connecting to $GATEWAY_URL") + val url = gatewayUrl + log("Connecting to $url") + DebugClient.lifecycle("DIRECT_GATEWAY", "Connecting to $url") val request = Request.Builder() - .url(GATEWAY_URL) + .url(url) .build() webSocket = client.newWebSocket(request, object : WebSocketListener() { override fun onOpen(ws: WebSocket, response: Response) { - Log.i(TAG, "Connected to gateway") - isConnected = true - DebugClient.lifecycle("DIRECT_GATEWAY", "Connected") - - // Send hello - send(mapOf( - "type" to "hello", - "client" to "clawdnode-android", - "version" to "0.1.0" - )) + log("WebSocket connected, waiting for welcome...") + DebugClient.lifecycle("DIRECT_GATEWAY", "Connected, waiting for welcome") + // Don't set isConnected yet - wait for protocol handshake } override fun onMessage(ws: WebSocket, text: String) { @@ -72,17 +89,19 @@ object DirectGateway { } override fun onClosed(ws: WebSocket, code: Int, reason: String) { - Log.i(TAG, "Connection closed: $code $reason") + log("Connection closed: $code $reason") isConnected = false webSocket = null + onConnectionChange?.invoke(false) DebugClient.lifecycle("DIRECT_GATEWAY", "Disconnected: $code $reason") scheduleReconnect() } override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) { - Log.e(TAG, "Connection failed", t) + log("Connection failed: ${t.message}") isConnected = false webSocket = null + onConnectionChange?.invoke(false) DebugClient.error("DirectGateway connection failed", t) scheduleReconnect() } @@ -110,11 +129,45 @@ object DirectGateway { val type = json.optString("type", "") when (type) { - "hello" -> { - Log.i(TAG, "Received hello from server") - DebugClient.log("DirectGateway hello received", mapOf( - "clientId" to json.optString("clientId") + "welcome" -> { + // Server welcome - send our hello + val protocol = json.optString("protocol", "unknown") + log("Received welcome (protocol: $protocol)") + DebugClient.log("DirectGateway welcome received", mapOf( + "protocol" to protocol )) + + // Check protocol compatibility + if (!protocol.startsWith("clawdnode/")) { + log("WARNING: Unexpected protocol: $protocol") + } + + // Send hello + webSocket?.send(JSONObject(mapOf( + "type" to "hello", + "client" to "clawdnode-android", + "version" to "0.1.0" + )).toString()) + } + + "ready" -> { + // Server confirmed connection + log("Connection ready: ${json.optString("message")}") + isConnected = true + onConnectionChange?.invoke(true) + DebugClient.lifecycle("DIRECT_GATEWAY", "Ready - fully connected") + } + + "error" -> { + // Server error + val code = json.optString("code") + val message = json.optString("message") + log("Server error [$code]: $message") + DebugClient.error("Gateway error: $code - $message", null) + + if (code == "WRONG_PROTOCOL") { + log("ERROR: Connected to wrong server! Check gateway URL and port.") + } } "command" -> { diff --git a/app/src/main/java/com/inou/clawdnode/security/TokenStore.kt b/app/src/main/java/com/inou/clawdnode/security/TokenStore.kt index d7abc66..87ff495 100644 --- a/app/src/main/java/com/inou/clawdnode/security/TokenStore.kt +++ b/app/src/main/java/com/inou/clawdnode/security/TokenStore.kt @@ -50,8 +50,8 @@ class TokenStore(context: Context) { private const val KEY_GATEWAY_TOKEN = "gateway_token" private const val KEY_NODE_ID = "node_id" - // Default values for testing - private const val DEFAULT_GATEWAY_URL = "ws://100.123.216.65:18789" - private const val DEFAULT_GATEWAY_TOKEN = "2dee57cc3ce2947c27ce9e848d5c3e95cc452f25a1477462" + // Default values - ClawdNode custom gateway (not Clawdbot) + private const val DEFAULT_GATEWAY_URL = "ws://100.123.216.65:9878" + private const val DEFAULT_GATEWAY_TOKEN = "" // No token needed for custom gateway } } diff --git a/app/src/main/java/com/inou/clawdnode/service/NodeService.kt b/app/src/main/java/com/inou/clawdnode/service/NodeService.kt index 587090d..a8baa6e 100644 --- a/app/src/main/java/com/inou/clawdnode/service/NodeService.kt +++ b/app/src/main/java/com/inou/clawdnode/service/NodeService.kt @@ -10,6 +10,7 @@ import android.util.Log import androidx.core.app.NotificationCompat import com.inou.clawdnode.ClawdNodeApp import com.inou.clawdnode.R +import com.inou.clawdnode.gateway.DirectGateway import com.inou.clawdnode.protocol.* import com.inou.clawdnode.ui.MainActivity @@ -22,8 +23,6 @@ class NodeService : Service() { private val tag = "NodeService" private val binder = LocalBinder() - private lateinit var gatewayClient: GatewayClient - private var isConnected = false // Callbacks for UI updates @@ -40,15 +39,29 @@ class NodeService : Service() { super.onCreate() Log.i(tag, "NodeService created") - gatewayClient = GatewayClient( - onCommand = { command -> handleCommand(command) }, - onConnectionChange = { connected -> - isConnected = connected - updateNotification() - onConnectionChange?.invoke(connected) - }, - onLog = { message -> onLogMessage?.invoke(message) } - ) + // Use DirectGateway (ClawdNode protocol) instead of GatewayClient (Clawdbot protocol) + DirectGateway.onConnectionChange = { connected -> + isConnected = connected + updateNotification() + onConnectionChange?.invoke(connected) + } + DirectGateway.onLog = { message -> + onLogMessage?.invoke(message) + } + + // Wire up command handlers + DirectGateway.onNotificationAction = { notificationId, action, replyText -> + NotificationManager.triggerAction(notificationId, action, replyText) + } + DirectGateway.onCallAnswer = { callId -> + CallManager.answer(callId, null) + } + DirectGateway.onCallReject = { callId -> + CallManager.reject(callId, null) + } + DirectGateway.onCallHangup = { callId -> + CallManager.hangup(callId) + } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -57,19 +70,15 @@ class NodeService : Service() { // Start as foreground service startForeground(NOTIFICATION_ID, createNotification()) - // Connect to gateway - if (ClawdNodeApp.instance.tokenStore.isConfigured) { - gatewayClient.connect() - } else { - Log.w(tag, "Gateway not configured, waiting for setup") - } + // Connect to gateway (DirectGateway uses URL from TokenStore) + DirectGateway.connect() return START_STICKY } override fun onDestroy() { Log.i(tag, "NodeService destroyed") - gatewayClient.disconnect() + DirectGateway.disconnect() ClawdNodeApp.instance.auditLog.log("SERVICE_STOP", "NodeService destroyed") super.onDestroy() } @@ -79,102 +88,16 @@ class NodeService : Service() { // ======================================== fun connect() { - gatewayClient.connect() + DirectGateway.connect() } fun disconnect() { - gatewayClient.disconnect() + DirectGateway.disconnect() } fun isConnected() = isConnected - fun sendEvent(event: NodeEvent) { - gatewayClient.send(event) - } - - // ======================================== - // COMMAND HANDLING - // ======================================== - - private fun handleCommand(command: NodeCommand) { - Log.d(tag, "Handling command: ${command::class.simpleName}") - - try { - when (command) { - is ScreenshotCommand -> handleScreenshot(command) - is NotificationActionCommand -> handleNotificationAction(command) - is NotificationDismissCommand -> handleNotificationDismiss(command) - is CallAnswerCommand -> handleCallAnswer(command) - is CallRejectCommand -> handleCallReject(command) - is CallSilenceCommand -> handleCallSilence(command) - is CallSpeakCommand -> handleCallSpeak(command) - is CallHangupCommand -> handleCallHangup(command) - } - } catch (e: Exception) { - Log.e(tag, "Error handling command", e) - command.requestId?.let { - gatewayClient.sendResponse(it, false, error = e.message) - } - } - } - - private fun handleScreenshot(cmd: ScreenshotCommand) { - // TODO: Implement screenshot capture via MediaProjection - Log.d(tag, "Screenshot requested - not yet implemented") - cmd.requestId?.let { - gatewayClient.sendResponse(it, false, error = "Screenshot not yet implemented") - } - } - - private fun handleNotificationAction(cmd: NotificationActionCommand) { - // Delegate to NotificationListener - NotificationManager.triggerAction(cmd.notificationId, cmd.action, cmd.text) - cmd.requestId?.let { - gatewayClient.sendResponse(it, true, payload = mapOf("triggered" to true)) - } - } - - private fun handleNotificationDismiss(cmd: NotificationDismissCommand) { - NotificationManager.dismiss(cmd.notificationId) - cmd.requestId?.let { - gatewayClient.sendResponse(it, true, payload = mapOf("dismissed" to true)) - } - } - - private fun handleCallAnswer(cmd: CallAnswerCommand) { - CallManager.answer(cmd.callId, cmd.greeting) - cmd.requestId?.let { - gatewayClient.sendResponse(it, true, payload = mapOf("answered" to true)) - } - } - - private fun handleCallReject(cmd: CallRejectCommand) { - CallManager.reject(cmd.callId, cmd.reason) - cmd.requestId?.let { - gatewayClient.sendResponse(it, true, payload = mapOf("rejected" to true)) - } - } - - private fun handleCallSilence(cmd: CallSilenceCommand) { - CallManager.silence(cmd.callId) - cmd.requestId?.let { - gatewayClient.sendResponse(it, true, payload = mapOf("silenced" to true)) - } - } - - private fun handleCallSpeak(cmd: CallSpeakCommand) { - CallManager.speak(cmd.callId, cmd.text, cmd.voice) - cmd.requestId?.let { - gatewayClient.sendResponse(it, true, payload = mapOf("speaking" to true)) - } - } - - private fun handleCallHangup(cmd: CallHangupCommand) { - CallManager.hangup(cmd.callId) - cmd.requestId?.let { - gatewayClient.sendResponse(it, true, payload = mapOf("hungup" to true)) - } - } + // Note: Command handling is done via DirectGateway callbacks set in onCreate() // ======================================== // NOTIFICATION