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
This commit is contained in:
parent
5eb13b01b5
commit
e0835e0626
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// 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? {
|
||||
|
|
|
|||
|
|
@ -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<String, Any?> = emptyMap()) {
|
||||
post("/log", mapOf("message" to message) + data)
|
||||
}
|
||||
|
||||
fun error(message: String, throwable: Throwable? = null, data: Map<String, Any?> = 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<String> = 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<String, Any?> = emptyMap()) {
|
||||
post("/event", mapOf("type" to type) + data)
|
||||
}
|
||||
|
||||
private fun post(endpoint: String, data: Map<String, Any?>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, StatusBarNotification>()
|
||||
|
||||
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")
|
||||
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
|
||||
try {
|
||||
Intent(this, NodeService::class.java).also { intent ->
|
||||
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||
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")
|
||||
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)
|
||||
|
||||
// POST directly to debug server
|
||||
DebugClient.notification(
|
||||
id = notificationId,
|
||||
app = appName,
|
||||
packageName = sbn.packageName,
|
||||
title = title,
|
||||
text = text,
|
||||
actions = actions
|
||||
)
|
||||
|
||||
// Also log to local audit
|
||||
try {
|
||||
ClawdNodeApp.instance.auditLog.logNotification(
|
||||
"NOTIFICATION_POSTED",
|
||||
sbn.packageName,
|
||||
event.title
|
||||
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)
|
||||
Log.i(tag, "Notification event sent to gateway")
|
||||
} 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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue