From e0835e0626ccb7d207a17e59c7b8b74ba93a9510 Mon Sep 17 00:00:00 2001 From: "James (ClawdBot)" Date: Wed, 28 Jan 2026 21:15:11 +0000 Subject: [PATCH] feat: Add direct HTTP debug logging - Add DebugClient that POSTs directly to debug server (100.123.216.65:9876) - NotificationListener: POST all events directly, full lifecycle logging - CallScreener: POST all calls directly, full lifecycle logging - App: Log startup and initialization - Bypass WebSocket complexity for debugging visibility Debug server: node /home/johan/dev/clawdnode-debug-server/server.js Tail: tail -f /tmp/clawdnode-debug.log --- .../java/com/inou/clawdnode/ClawdNodeApp.kt | 8 + .../com/inou/clawdnode/calls/CallScreener.kt | 34 +++- .../com/inou/clawdnode/debug/DebugClient.kt | 131 ++++++++++++ .../notifications/NotificationListener.kt | 191 ++++++++++++------ 4 files changed, 299 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/com/inou/clawdnode/debug/DebugClient.kt diff --git a/app/src/main/java/com/inou/clawdnode/ClawdNodeApp.kt b/app/src/main/java/com/inou/clawdnode/ClawdNodeApp.kt index 39f7af7..8eff2c9 100644 --- a/app/src/main/java/com/inou/clawdnode/ClawdNodeApp.kt +++ b/app/src/main/java/com/inou/clawdnode/ClawdNodeApp.kt @@ -4,6 +4,7 @@ import android.app.Application import android.app.NotificationChannel import android.app.NotificationManager import android.os.Build +import com.inou.clawdnode.debug.DebugClient import com.inou.clawdnode.security.AuditLog import com.inou.clawdnode.security.DeviceIdentity import com.inou.clawdnode.security.TokenStore @@ -34,6 +35,13 @@ class ClawdNodeApp : Application() { createNotificationChannels() auditLog.log("APP_START", "ClawdNode v0.1.0 started") + + // Initialize debug client and log startup + DebugClient.lifecycle("APP_CREATE", "ClawdNode v0.1.0 started") + DebugClient.log("App initialized", mapOf( + "gatewayUrl" to (tokenStore.gatewayUrl ?: "not set"), + "hasToken" to (tokenStore.gatewayToken != null) + )) } private fun createNotificationChannels() { diff --git a/app/src/main/java/com/inou/clawdnode/calls/CallScreener.kt b/app/src/main/java/com/inou/clawdnode/calls/CallScreener.kt index f29174f..4cf57b5 100644 --- a/app/src/main/java/com/inou/clawdnode/calls/CallScreener.kt +++ b/app/src/main/java/com/inou/clawdnode/calls/CallScreener.kt @@ -9,6 +9,7 @@ import android.telecom.Call import android.telecom.CallScreeningService import android.util.Log import com.inou.clawdnode.ClawdNodeApp +import com.inou.clawdnode.debug.DebugClient import com.inou.clawdnode.protocol.CallIncomingEvent import com.inou.clawdnode.service.NodeService @@ -33,9 +34,11 @@ class CallScreener : CallScreeningService() { override fun onCreate() { super.onCreate() Log.i(tag, "CallScreener created") + DebugClient.lifecycle("CALL_SCREENER_CREATE", "Service started") Intent(this, NodeService::class.java).also { intent -> - bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + val bound = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + DebugClient.lifecycle("CALL_SCREENER", "bindService returned: $bound") } } @@ -50,19 +53,32 @@ class CallScreener : CallScreeningService() { Log.i(tag, "Screening call from: $number") + // POST to debug server immediately + DebugClient.call( + callId = callId, + number = number, + contact = null, + state = "incoming" + ) + // Look up contact name val contactName = lookupContact(number) // Store call details for later action ActiveCalls.add(callId, callDetails) - // Send event to Gateway - val event = CallIncomingEvent( - callId = callId, - number = number, - contact = contactName - ) - nodeService?.sendEvent(event) + // Send event to Gateway via WebSocket + try { + val event = CallIncomingEvent( + callId = callId, + number = number, + contact = contactName + ) + nodeService?.sendEvent(event) + DebugClient.log("Call event sent to gateway", mapOf("callId" to callId)) + } catch (e: Exception) { + DebugClient.error("Failed to send call event", e) + } ClawdNodeApp.instance.auditLog.logCall( "CALL_INCOMING", @@ -80,6 +96,8 @@ class CallScreener : CallScreeningService() { .setSkipNotification(false) .build() ) + + DebugClient.call(callId, number, contactName, "allowed_to_ring") } private fun lookupContact(number: String): String? { diff --git a/app/src/main/java/com/inou/clawdnode/debug/DebugClient.kt b/app/src/main/java/com/inou/clawdnode/debug/DebugClient.kt new file mode 100644 index 0000000..26fa7a6 --- /dev/null +++ b/app/src/main/java/com/inou/clawdnode/debug/DebugClient.kt @@ -0,0 +1,131 @@ +package com.inou.clawdnode.debug + +import android.util.Log +import kotlinx.coroutines.* +import okhttp3.* +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.toRequestBody +import org.json.JSONObject +import java.io.IOException + +/** + * Direct HTTP client for debugging. + * Posts events directly to the debug server, bypassing WebSocket complexity. + */ +object DebugClient { + private const val TAG = "DebugClient" + + // Debug server URL - Tailscale IP of james server + private const val DEBUG_SERVER = "http://100.123.216.65:9876" + + private val client = OkHttpClient.Builder() + .connectTimeout(5, java.util.concurrent.TimeUnit.SECONDS) + .writeTimeout(5, java.util.concurrent.TimeUnit.SECONDS) + .readTimeout(5, java.util.concurrent.TimeUnit.SECONDS) + .build() + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + private val JSON = "application/json".toMediaType() + + var enabled = true + + fun log(message: String, data: Map = emptyMap()) { + post("/log", mapOf("message" to message) + data) + } + + fun error(message: String, throwable: Throwable? = null, data: Map = emptyMap()) { + post("/error", mapOf( + "message" to message, + "error" to (throwable?.message ?: ""), + "stack" to (throwable?.stackTraceToString()?.take(500) ?: "") + ) + data) + } + + fun lifecycle(event: String, message: String = "") { + post("/lifecycle", mapOf("event" to event, "message" to message)) + } + + fun notification( + id: String, + app: String, + packageName: String, + title: String?, + text: String?, + actions: List = emptyList() + ) { + post("/notification", mapOf( + "id" to id, + "app" to app, + "packageName" to packageName, + "title" to title, + "text" to text, + "actions" to actions, + "timestamp" to System.currentTimeMillis() + )) + } + + fun call( + callId: String, + number: String?, + contact: String?, + state: String + ) { + post("/call", mapOf( + "callId" to callId, + "number" to number, + "contact" to contact, + "state" to state, + "timestamp" to System.currentTimeMillis() + )) + } + + fun event(type: String, data: Map = emptyMap()) { + post("/event", mapOf("type" to type) + data) + } + + private fun post(endpoint: String, data: Map) { + if (!enabled) return + + scope.launch { + try { + val json = JSONObject(data).toString() + val request = Request.Builder() + .url("$DEBUG_SERVER$endpoint") + .post(json.toRequestBody(JSON)) + .build() + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + Log.w(TAG, "Debug post failed: ${response.code}") + } + } + } catch (e: IOException) { + // Silently fail - debug server might not be running + Log.d(TAG, "Debug post failed: ${e.message}") + } catch (e: Exception) { + Log.e(TAG, "Debug post error", e) + } + } + } + + fun testConnection(callback: (Boolean) -> Unit) { + scope.launch { + try { + val request = Request.Builder() + .url("$DEBUG_SERVER/health") + .get() + .build() + + client.newCall(request).execute().use { response -> + withContext(Dispatchers.Main) { + callback(response.isSuccessful) + } + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + callback(false) + } + } + } + } +} diff --git a/app/src/main/java/com/inou/clawdnode/notifications/NotificationListener.kt b/app/src/main/java/com/inou/clawdnode/notifications/NotificationListener.kt index 1219819..93f668d 100644 --- a/app/src/main/java/com/inou/clawdnode/notifications/NotificationListener.kt +++ b/app/src/main/java/com/inou/clawdnode/notifications/NotificationListener.kt @@ -12,13 +12,14 @@ import android.service.notification.NotificationListenerService import android.service.notification.StatusBarNotification import android.util.Log import com.inou.clawdnode.ClawdNodeApp +import com.inou.clawdnode.debug.DebugClient import com.inou.clawdnode.protocol.NotificationEvent import com.inou.clawdnode.service.NodeService import com.inou.clawdnode.service.NotificationManager /** * Listens to all notifications and forwards them to Gateway. - * Can also trigger notification actions (reply, dismiss, etc.) + * Also posts directly to debug server for visibility. */ class NotificationListener : NotificationListenerService() { @@ -28,74 +29,159 @@ class NotificationListener : NotificationListenerService() { private val activeNotifications = mutableMapOf() private var nodeService: NodeService? = null + private var serviceConnected = false + private val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.i(tag, "NodeService connected") + DebugClient.lifecycle("NOTIFICATION_LISTENER", "NodeService bound successfully") nodeService = (service as NodeService.LocalBinder).getService() + serviceConnected = true } override fun onServiceDisconnected(name: ComponentName?) { + Log.i(tag, "NodeService disconnected") + DebugClient.lifecycle("NOTIFICATION_LISTENER", "NodeService disconnected") nodeService = null + serviceConnected = false } } override fun onCreate() { super.onCreate() Log.i(tag, "NotificationListener created") - NotificationManager.register(this) + DebugClient.lifecycle("NOTIFICATION_LISTENER_CREATE", "Service onCreate called") + + try { + NotificationManager.register(this) + DebugClient.lifecycle("NOTIFICATION_LISTENER", "Registered with NotificationManager") + } catch (e: Exception) { + Log.e(tag, "Failed to register with NotificationManager", e) + DebugClient.error("NotificationManager registration failed", e) + } // Bind to NodeService - Intent(this, NodeService::class.java).also { intent -> - bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + try { + Intent(this, NodeService::class.java).also { intent -> + val bound = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + Log.i(tag, "bindService returned: $bound") + DebugClient.lifecycle("NOTIFICATION_LISTENER", "bindService returned: $bound") + } + } catch (e: Exception) { + Log.e(tag, "Failed to bind to NodeService", e) + DebugClient.error("NodeService bind failed", e) } } + override fun onListenerConnected() { + super.onListenerConnected() + Log.i(tag, "NotificationListener connected to system") + DebugClient.lifecycle("NOTIFICATION_LISTENER_CONNECTED", "System has connected us to notification stream") + + // Log existing notifications + try { + val existing = activeNotifications.size + DebugClient.log("Listener connected, ${existing} active notifications tracked") + } catch (e: Exception) { + DebugClient.error("Error in onListenerConnected", e) + } + } + + override fun onListenerDisconnected() { + super.onListenerDisconnected() + Log.w(tag, "NotificationListener disconnected from system") + DebugClient.lifecycle("NOTIFICATION_LISTENER_DISCONNECTED", "System has disconnected us from notification stream") + } + override fun onDestroy() { Log.i(tag, "NotificationListener destroyed") - unbindService(serviceConnection) + DebugClient.lifecycle("NOTIFICATION_LISTENER_DESTROY", "Service onDestroy called") + try { + unbindService(serviceConnection) + } catch (e: Exception) { + // Ignore + } super.onDestroy() } override fun onNotificationPosted(sbn: StatusBarNotification) { - // Log immediately to confirm listener is working - Log.i(tag, "onNotificationPosted called: ${sbn.packageName}") - ClawdNodeApp.instance.auditLog.log( - "NOTIFICATION_RAW", - "Received: ${sbn.packageName}", - mapOf("id" to sbn.id.toString(), "key" to sbn.key) - ) + // ALWAYS log to debug server first + Log.i(tag, "onNotificationPosted: ${sbn.packageName}") + DebugClient.log("onNotificationPosted called", mapOf( + "package" to sbn.packageName, + "id" to sbn.id, + "key" to sbn.key, + "isOngoing" to sbn.isOngoing + )) // Skip our own notifications if (sbn.packageName == packageName) { - Log.d(tag, "Skipping own notification") + DebugClient.log("Skipping own notification") return } // Skip ongoing/persistent notifications (media players, etc.) if (sbn.isOngoing && !isImportantOngoing(sbn)) { - Log.d(tag, "Skipping ongoing notification") + DebugClient.log("Skipping ongoing notification", mapOf("package" to sbn.packageName)) return } - Log.d(tag, "Processing notification: ${sbn.packageName}") - val notificationId = generateNotificationId(sbn) activeNotifications[notificationId] = sbn - val event = createNotificationEvent(sbn, notificationId) + // Extract notification data + val notification = sbn.notification + val extras = notification.extras + val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString() + val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() + ?: extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString() + val actions = notification.actions?.map { it.title.toString() } ?: emptyList() + val appName = getAppName(sbn.packageName) - ClawdNodeApp.instance.auditLog.logNotification( - "NOTIFICATION_POSTED", - sbn.packageName, - event.title + // POST directly to debug server + DebugClient.notification( + id = notificationId, + app = appName, + packageName = sbn.packageName, + title = title, + text = text, + actions = actions ) - nodeService?.sendEvent(event) - Log.i(tag, "Notification event sent to gateway") + // Also log to local audit + try { + ClawdNodeApp.instance.auditLog.logNotification( + "NOTIFICATION_POSTED", + sbn.packageName, + title + ) + } catch (e: Exception) { + DebugClient.error("Audit log failed", e) + } + + // Send via WebSocket if connected + try { + val event = NotificationEvent( + id = notificationId, + app = appName, + packageName = sbn.packageName, + title = title, + text = text, + actions = actions, + timestamp = sbn.postTime + ) + nodeService?.sendEvent(event) + } catch (e: Exception) { + DebugClient.error("WebSocket send failed", e) + } } override fun onNotificationRemoved(sbn: StatusBarNotification) { val notificationId = generateNotificationId(sbn) activeNotifications.remove(notificationId) - Log.d(tag, "Notification removed: ${sbn.packageName}") + DebugClient.log("Notification removed", mapOf( + "package" to sbn.packageName, + "id" to notificationId + )) } // ======================================== @@ -103,8 +189,15 @@ class NotificationListener : NotificationListenerService() { // ======================================== fun triggerAction(notificationId: String, actionTitle: String, replyText: String?) { + DebugClient.log("triggerAction called", mapOf( + "notificationId" to notificationId, + "action" to actionTitle, + "hasReply" to (replyText != null) + )) + val sbn = activeNotifications[notificationId] ?: run { Log.w(tag, "Notification not found: $notificationId") + DebugClient.error("Notification not found for action", null, mapOf("id" to notificationId)) return } @@ -114,18 +207,23 @@ class NotificationListener : NotificationListenerService() { val action = actions.find { it.title.toString().equals(actionTitle, ignoreCase = true) } if (action == null) { Log.w(tag, "Action not found: $actionTitle") + DebugClient.error("Action not found", null, mapOf("action" to actionTitle)) return } try { if (replyText != null && action.remoteInputs?.isNotEmpty() == true) { - // This is a reply action sendReply(action, replyText) } else { - // Regular action action.actionIntent.send() } + DebugClient.event("NOTIFICATION_ACTION_TRIGGERED", mapOf( + "notificationId" to notificationId, + "action" to actionTitle, + "isReply" to (replyText != null) + )) + ClawdNodeApp.instance.auditLog.log( "NOTIFICATION_ACTION", "Triggered action: $actionTitle", @@ -133,18 +231,15 @@ class NotificationListener : NotificationListenerService() { ) } catch (e: Exception) { Log.e(tag, "Failed to trigger action", e) + DebugClient.error("Action trigger failed", e) } } fun dismissNotification(notificationId: String) { + DebugClient.log("dismissNotification called", mapOf("id" to notificationId)) val sbn = activeNotifications[notificationId] ?: return cancelNotification(sbn.key) - - ClawdNodeApp.instance.auditLog.log( - "NOTIFICATION_DISMISS", - "Dismissed notification", - mapOf("notification_id" to notificationId) - ) + DebugClient.event("NOTIFICATION_DISMISSED", mapOf("id" to notificationId)) } private fun sendReply(action: Notification.Action, text: String) { @@ -156,6 +251,7 @@ class NotificationListener : NotificationListenerService() { RemoteInput.addResultsToIntent(arrayOf(remoteInput), intent, bundle) action.actionIntent.send(this, 0, intent) + DebugClient.event("NOTIFICATION_REPLY_SENT", mapOf("textLength" to text.length)) } // ======================================== @@ -166,40 +262,21 @@ class NotificationListener : NotificationListenerService() { return "${sbn.packageName}:${sbn.id}:${sbn.postTime}" } - private fun createNotificationEvent(sbn: StatusBarNotification, id: String): NotificationEvent { - val notification = sbn.notification - val extras = notification.extras - - val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString() - val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() - ?: extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString() - - val actions = notification.actions?.map { it.title.toString() } ?: emptyList() - - val appName = try { - val appInfo = packageManager.getApplicationInfo(sbn.packageName, 0) + private fun getAppName(packageName: String): String { + return try { + val appInfo = packageManager.getApplicationInfo(packageName, 0) packageManager.getApplicationLabel(appInfo).toString() } catch (e: Exception) { - sbn.packageName + packageName } - - return NotificationEvent( - id = id, - app = appName, - packageName = sbn.packageName, - title = title, - text = text, - actions = actions, - timestamp = sbn.postTime - ) } private fun isImportantOngoing(sbn: StatusBarNotification): Boolean { - // Whitelist certain ongoing notifications we care about val importantPackages = setOf( "com.whatsapp", "org.telegram.messenger", - "com.google.android.apps.messaging" + "com.google.android.apps.messaging", + "org.thoughtcrime.securesms" // Signal ) return sbn.packageName in importantPackages }