feat: Add DirectGateway - our own WebSocket server
- DirectGateway.kt: bidirectional WebSocket to ws://100.123.216.65:9878 - No auth, no restrictions - full control - Sends notifications and calls - Receives commands: notification.action, call.answer/reject/hangup - App sets up command handlers and auto-connects - NotificationListener & CallScreener now send to both debug + gateway Server: /home/johan/dev/clawdnode-gateway/server.js HTTP API: http://100.123.216.65:9877 WebSocket: ws://100.123.216.65:9878
This commit is contained in:
parent
e0835e0626
commit
e3b68c9c21
|
|
@ -5,9 +5,11 @@ import android.app.NotificationChannel
|
|||
import android.app.NotificationManager
|
||||
import android.os.Build
|
||||
import com.inou.clawdnode.debug.DebugClient
|
||||
import com.inou.clawdnode.gateway.DirectGateway
|
||||
import com.inou.clawdnode.security.AuditLog
|
||||
import com.inou.clawdnode.security.DeviceIdentity
|
||||
import com.inou.clawdnode.security.TokenStore
|
||||
import com.inou.clawdnode.service.NotificationManager
|
||||
|
||||
/**
|
||||
* ClawdNode Application
|
||||
|
|
@ -42,6 +44,36 @@ class ClawdNodeApp : Application() {
|
|||
"gatewayUrl" to (tokenStore.gatewayUrl ?: "not set"),
|
||||
"hasToken" to (tokenStore.gatewayToken != null)
|
||||
))
|
||||
|
||||
// Connect to our DirectGateway (bidirectional WebSocket)
|
||||
setupDirectGateway()
|
||||
}
|
||||
|
||||
private fun setupDirectGateway() {
|
||||
// Set up command handlers
|
||||
DirectGateway.onNotificationAction = { notificationId, action, replyText ->
|
||||
auditLog.log("COMMAND_RECEIVED", "notification.action: $action on $notificationId")
|
||||
NotificationManager.getInstance()?.triggerAction(notificationId, action, replyText)
|
||||
}
|
||||
|
||||
DirectGateway.onCallAnswer = { callId ->
|
||||
auditLog.log("COMMAND_RECEIVED", "call.answer: $callId")
|
||||
// TODO: Implement call answering via TelecomManager
|
||||
}
|
||||
|
||||
DirectGateway.onCallReject = { callId ->
|
||||
auditLog.log("COMMAND_RECEIVED", "call.reject: $callId")
|
||||
// TODO: Implement call rejection
|
||||
}
|
||||
|
||||
DirectGateway.onCallHangup = { callId ->
|
||||
auditLog.log("COMMAND_RECEIVED", "call.hangup: $callId")
|
||||
// TODO: Implement call hangup
|
||||
}
|
||||
|
||||
// Connect
|
||||
DirectGateway.connect()
|
||||
DebugClient.lifecycle("DIRECT_GATEWAY_SETUP", "Command handlers registered, connecting...")
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import android.telecom.CallScreeningService
|
|||
import android.util.Log
|
||||
import com.inou.clawdnode.ClawdNodeApp
|
||||
import com.inou.clawdnode.debug.DebugClient
|
||||
import com.inou.clawdnode.gateway.DirectGateway
|
||||
import com.inou.clawdnode.protocol.CallIncomingEvent
|
||||
import com.inou.clawdnode.service.NodeService
|
||||
|
||||
|
|
@ -61,6 +62,9 @@ class CallScreener : CallScreeningService() {
|
|||
state = "incoming"
|
||||
)
|
||||
|
||||
// Send via DirectGateway
|
||||
DirectGateway.sendCall(callId, number, null, "incoming")
|
||||
|
||||
// Look up contact name
|
||||
val contactName = lookupContact(number)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,266 @@
|
|||
package com.inou.clawdnode.gateway
|
||||
|
||||
import android.util.Log
|
||||
import com.inou.clawdnode.ClawdNodeApp
|
||||
import com.inou.clawdnode.debug.DebugClient
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.*
|
||||
import org.json.JSONObject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Direct WebSocket connection to our own ClawdNode Gateway.
|
||||
* No authentication, no restrictions - full bidirectional control.
|
||||
*/
|
||||
object DirectGateway {
|
||||
private const val TAG = "DirectGateway"
|
||||
|
||||
// Our gateway - Tailscale IP of james server
|
||||
private const val GATEWAY_URL = "ws://100.123.216.65:9878"
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(0, TimeUnit.SECONDS)
|
||||
.pingInterval(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private var webSocket: WebSocket? = null
|
||||
private var isConnected = false
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
// Command handlers
|
||||
var onNotificationAction: ((notificationId: String, action: String, replyText: String?) -> Unit)? = null
|
||||
var onCallAnswer: ((callId: String) -> Unit)? = null
|
||||
var onCallReject: ((callId: String) -> Unit)? = null
|
||||
var onCallHangup: ((callId: String) -> Unit)? = null
|
||||
|
||||
fun connect() {
|
||||
if (webSocket != null) {
|
||||
Log.d(TAG, "Already connected or connecting")
|
||||
return
|
||||
}
|
||||
|
||||
Log.i(TAG, "Connecting to $GATEWAY_URL")
|
||||
DebugClient.lifecycle("DIRECT_GATEWAY", "Connecting to $GATEWAY_URL")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(GATEWAY_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"
|
||||
))
|
||||
}
|
||||
|
||||
override fun onMessage(ws: WebSocket, text: String) {
|
||||
Log.d(TAG, "Received: $text")
|
||||
handleMessage(text)
|
||||
}
|
||||
|
||||
override fun onClosing(ws: WebSocket, code: Int, reason: String) {
|
||||
Log.i(TAG, "Connection closing: $code $reason")
|
||||
ws.close(1000, null)
|
||||
}
|
||||
|
||||
override fun onClosed(ws: WebSocket, code: Int, reason: String) {
|
||||
Log.i(TAG, "Connection closed: $code $reason")
|
||||
isConnected = false
|
||||
webSocket = null
|
||||
DebugClient.lifecycle("DIRECT_GATEWAY", "Disconnected: $code $reason")
|
||||
scheduleReconnect()
|
||||
}
|
||||
|
||||
override fun onFailure(ws: WebSocket, t: Throwable, response: Response?) {
|
||||
Log.e(TAG, "Connection failed", t)
|
||||
isConnected = false
|
||||
webSocket = null
|
||||
DebugClient.error("DirectGateway connection failed", t)
|
||||
scheduleReconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun disconnect() {
|
||||
webSocket?.close(1000, "Client disconnect")
|
||||
webSocket = null
|
||||
isConnected = false
|
||||
}
|
||||
|
||||
private fun scheduleReconnect() {
|
||||
scope.launch {
|
||||
delay(5000)
|
||||
if (webSocket == null) {
|
||||
connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMessage(text: String) {
|
||||
try {
|
||||
val json = JSONObject(text)
|
||||
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")
|
||||
))
|
||||
}
|
||||
|
||||
"command" -> {
|
||||
val command = json.optString("command")
|
||||
val params = json.optJSONObject("params") ?: JSONObject()
|
||||
val commandId = json.optString("commandId")
|
||||
|
||||
Log.i(TAG, "Received command: $command")
|
||||
DebugClient.log("Command received", mapOf(
|
||||
"command" to command,
|
||||
"commandId" to commandId
|
||||
))
|
||||
|
||||
handleCommand(command, params, commandId)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error parsing message", e)
|
||||
DebugClient.error("DirectGateway parse error", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCommand(command: String, params: JSONObject, commandId: String) {
|
||||
try {
|
||||
when (command) {
|
||||
"notification.action" -> {
|
||||
val notificationId = params.optString("notificationId")
|
||||
val action = params.optString("action")
|
||||
val replyText = params.optString("replyText", null)
|
||||
|
||||
Log.i(TAG, "Triggering notification action: $action on $notificationId")
|
||||
onNotificationAction?.invoke(notificationId, action, replyText)
|
||||
|
||||
sendResponse(commandId, true)
|
||||
ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "notification.action: $action")
|
||||
}
|
||||
|
||||
"call.answer" -> {
|
||||
val callId = params.optString("callId")
|
||||
Log.i(TAG, "Answering call: $callId")
|
||||
onCallAnswer?.invoke(callId)
|
||||
|
||||
sendResponse(commandId, true)
|
||||
ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "call.answer")
|
||||
}
|
||||
|
||||
"call.reject" -> {
|
||||
val callId = params.optString("callId")
|
||||
Log.i(TAG, "Rejecting call: $callId")
|
||||
onCallReject?.invoke(callId)
|
||||
|
||||
sendResponse(commandId, true)
|
||||
ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "call.reject")
|
||||
}
|
||||
|
||||
"call.hangup" -> {
|
||||
val callId = params.optString("callId")
|
||||
Log.i(TAG, "Hanging up call: $callId")
|
||||
onCallHangup?.invoke(callId)
|
||||
|
||||
sendResponse(commandId, true)
|
||||
ClawdNodeApp.instance.auditLog.log("COMMAND_EXECUTED", "call.hangup")
|
||||
}
|
||||
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown command: $command")
|
||||
sendResponse(commandId, false, "Unknown command: $command")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error executing command", e)
|
||||
sendResponse(commandId, false, e.message)
|
||||
DebugClient.error("Command execution failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendResponse(commandId: String, success: Boolean, error: String? = null) {
|
||||
send(mapOf(
|
||||
"type" to "response",
|
||||
"commandId" to commandId,
|
||||
"success" to success,
|
||||
"error" to error
|
||||
))
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Outgoing events
|
||||
// ========================================
|
||||
|
||||
fun sendNotification(
|
||||
id: String,
|
||||
app: String,
|
||||
packageName: String,
|
||||
title: String?,
|
||||
text: String?,
|
||||
actions: List<String>
|
||||
) {
|
||||
send(mapOf(
|
||||
"type" to "notification",
|
||||
"id" to id,
|
||||
"app" to app,
|
||||
"packageName" to packageName,
|
||||
"title" to title,
|
||||
"text" to text,
|
||||
"actions" to actions,
|
||||
"timestamp" to System.currentTimeMillis()
|
||||
))
|
||||
}
|
||||
|
||||
fun sendCall(
|
||||
callId: String,
|
||||
number: String?,
|
||||
contact: String?,
|
||||
state: String
|
||||
) {
|
||||
send(mapOf(
|
||||
"type" to "call",
|
||||
"callId" to callId,
|
||||
"number" to number,
|
||||
"contact" to contact,
|
||||
"state" to state,
|
||||
"timestamp" to System.currentTimeMillis()
|
||||
))
|
||||
}
|
||||
|
||||
fun sendLog(message: String, data: Map<String, Any?> = emptyMap()) {
|
||||
send(mapOf("type" to "log", "message" to message) + data)
|
||||
}
|
||||
|
||||
fun sendLifecycle(event: String, message: String = "") {
|
||||
send(mapOf("type" to "lifecycle", "event" to event, "message" to message))
|
||||
}
|
||||
|
||||
private fun send(data: Map<String, Any?>) {
|
||||
if (!isConnected) {
|
||||
Log.d(TAG, "Not connected, cannot send")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
val json = JSONObject(data).toString()
|
||||
webSocket?.send(json)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Send failed", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun isConnected() = isConnected
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import android.service.notification.StatusBarNotification
|
|||
import android.util.Log
|
||||
import com.inou.clawdnode.ClawdNodeApp
|
||||
import com.inou.clawdnode.debug.DebugClient
|
||||
import com.inou.clawdnode.gateway.DirectGateway
|
||||
import com.inou.clawdnode.protocol.NotificationEvent
|
||||
import com.inou.clawdnode.service.NodeService
|
||||
import com.inou.clawdnode.service.NotificationManager
|
||||
|
|
@ -147,6 +148,16 @@ class NotificationListener : NotificationListenerService() {
|
|||
actions = actions
|
||||
)
|
||||
|
||||
// Send via DirectGateway (bidirectional WebSocket)
|
||||
DirectGateway.sendNotification(
|
||||
id = notificationId,
|
||||
app = appName,
|
||||
packageName = sbn.packageName,
|
||||
title = title,
|
||||
text = text,
|
||||
actions = actions
|
||||
)
|
||||
|
||||
// Also log to local audit
|
||||
try {
|
||||
ClawdNodeApp.instance.auditLog.logNotification(
|
||||
|
|
|
|||
Loading…
Reference in New Issue