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.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import com.inou.clawdnode.debug.DebugClient
|
||||||
import com.inou.clawdnode.security.AuditLog
|
import com.inou.clawdnode.security.AuditLog
|
||||||
import com.inou.clawdnode.security.DeviceIdentity
|
import com.inou.clawdnode.security.DeviceIdentity
|
||||||
import com.inou.clawdnode.security.TokenStore
|
import com.inou.clawdnode.security.TokenStore
|
||||||
|
|
@ -34,6 +35,13 @@ class ClawdNodeApp : Application() {
|
||||||
createNotificationChannels()
|
createNotificationChannels()
|
||||||
|
|
||||||
auditLog.log("APP_START", "ClawdNode v0.1.0 started")
|
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() {
|
private fun createNotificationChannels() {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import android.telecom.Call
|
||||||
import android.telecom.CallScreeningService
|
import android.telecom.CallScreeningService
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.inou.clawdnode.ClawdNodeApp
|
import com.inou.clawdnode.ClawdNodeApp
|
||||||
|
import com.inou.clawdnode.debug.DebugClient
|
||||||
import com.inou.clawdnode.protocol.CallIncomingEvent
|
import com.inou.clawdnode.protocol.CallIncomingEvent
|
||||||
import com.inou.clawdnode.service.NodeService
|
import com.inou.clawdnode.service.NodeService
|
||||||
|
|
||||||
|
|
@ -33,9 +34,11 @@ class CallScreener : CallScreeningService() {
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Log.i(tag, "CallScreener created")
|
Log.i(tag, "CallScreener created")
|
||||||
|
DebugClient.lifecycle("CALL_SCREENER_CREATE", "Service started")
|
||||||
|
|
||||||
Intent(this, NodeService::class.java).also { intent ->
|
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")
|
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
|
// Look up contact name
|
||||||
val contactName = lookupContact(number)
|
val contactName = lookupContact(number)
|
||||||
|
|
||||||
// Store call details for later action
|
// Store call details for later action
|
||||||
ActiveCalls.add(callId, callDetails)
|
ActiveCalls.add(callId, callDetails)
|
||||||
|
|
||||||
// Send event to Gateway
|
// Send event to Gateway via WebSocket
|
||||||
|
try {
|
||||||
val event = CallIncomingEvent(
|
val event = CallIncomingEvent(
|
||||||
callId = callId,
|
callId = callId,
|
||||||
number = number,
|
number = number,
|
||||||
contact = contactName
|
contact = contactName
|
||||||
)
|
)
|
||||||
nodeService?.sendEvent(event)
|
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(
|
ClawdNodeApp.instance.auditLog.logCall(
|
||||||
"CALL_INCOMING",
|
"CALL_INCOMING",
|
||||||
|
|
@ -80,6 +96,8 @@ class CallScreener : CallScreeningService() {
|
||||||
.setSkipNotification(false)
|
.setSkipNotification(false)
|
||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DebugClient.call(callId, number, contactName, "allowed_to_ring")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun lookupContact(number: String): String? {
|
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.service.notification.StatusBarNotification
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.inou.clawdnode.ClawdNodeApp
|
import com.inou.clawdnode.ClawdNodeApp
|
||||||
|
import com.inou.clawdnode.debug.DebugClient
|
||||||
import com.inou.clawdnode.protocol.NotificationEvent
|
import com.inou.clawdnode.protocol.NotificationEvent
|
||||||
import com.inou.clawdnode.service.NodeService
|
import com.inou.clawdnode.service.NodeService
|
||||||
import com.inou.clawdnode.service.NotificationManager
|
import com.inou.clawdnode.service.NotificationManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listens to all notifications and forwards them to Gateway.
|
* 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() {
|
class NotificationListener : NotificationListenerService() {
|
||||||
|
|
||||||
|
|
@ -28,74 +29,159 @@ class NotificationListener : NotificationListenerService() {
|
||||||
private val activeNotifications = mutableMapOf<String, StatusBarNotification>()
|
private val activeNotifications = mutableMapOf<String, StatusBarNotification>()
|
||||||
|
|
||||||
private var nodeService: NodeService? = null
|
private var nodeService: NodeService? = null
|
||||||
|
private var serviceConnected = false
|
||||||
|
|
||||||
private val serviceConnection = object : ServiceConnection {
|
private val serviceConnection = object : ServiceConnection {
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
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()
|
nodeService = (service as NodeService.LocalBinder).getService()
|
||||||
|
serviceConnected = true
|
||||||
}
|
}
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
Log.i(tag, "NodeService disconnected")
|
||||||
|
DebugClient.lifecycle("NOTIFICATION_LISTENER", "NodeService disconnected")
|
||||||
nodeService = null
|
nodeService = null
|
||||||
|
serviceConnected = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Log.i(tag, "NotificationListener created")
|
Log.i(tag, "NotificationListener created")
|
||||||
|
DebugClient.lifecycle("NOTIFICATION_LISTENER_CREATE", "Service onCreate called")
|
||||||
|
|
||||||
|
try {
|
||||||
NotificationManager.register(this)
|
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
|
// Bind to NodeService
|
||||||
|
try {
|
||||||
Intent(this, NodeService::class.java).also { intent ->
|
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() {
|
override fun onDestroy() {
|
||||||
Log.i(tag, "NotificationListener destroyed")
|
Log.i(tag, "NotificationListener destroyed")
|
||||||
|
DebugClient.lifecycle("NOTIFICATION_LISTENER_DESTROY", "Service onDestroy called")
|
||||||
|
try {
|
||||||
unbindService(serviceConnection)
|
unbindService(serviceConnection)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||||
// Log immediately to confirm listener is working
|
// ALWAYS log to debug server first
|
||||||
Log.i(tag, "onNotificationPosted called: ${sbn.packageName}")
|
Log.i(tag, "onNotificationPosted: ${sbn.packageName}")
|
||||||
ClawdNodeApp.instance.auditLog.log(
|
DebugClient.log("onNotificationPosted called", mapOf(
|
||||||
"NOTIFICATION_RAW",
|
"package" to sbn.packageName,
|
||||||
"Received: ${sbn.packageName}",
|
"id" to sbn.id,
|
||||||
mapOf("id" to sbn.id.toString(), "key" to sbn.key)
|
"key" to sbn.key,
|
||||||
)
|
"isOngoing" to sbn.isOngoing
|
||||||
|
))
|
||||||
|
|
||||||
// Skip our own notifications
|
// Skip our own notifications
|
||||||
if (sbn.packageName == packageName) {
|
if (sbn.packageName == packageName) {
|
||||||
Log.d(tag, "Skipping own notification")
|
DebugClient.log("Skipping own notification")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip ongoing/persistent notifications (media players, etc.)
|
// Skip ongoing/persistent notifications (media players, etc.)
|
||||||
if (sbn.isOngoing && !isImportantOngoing(sbn)) {
|
if (sbn.isOngoing && !isImportantOngoing(sbn)) {
|
||||||
Log.d(tag, "Skipping ongoing notification")
|
DebugClient.log("Skipping ongoing notification", mapOf("package" to sbn.packageName))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(tag, "Processing notification: ${sbn.packageName}")
|
|
||||||
|
|
||||||
val notificationId = generateNotificationId(sbn)
|
val notificationId = generateNotificationId(sbn)
|
||||||
activeNotifications[notificationId] = 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(
|
ClawdNodeApp.instance.auditLog.logNotification(
|
||||||
"NOTIFICATION_POSTED",
|
"NOTIFICATION_POSTED",
|
||||||
sbn.packageName,
|
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)
|
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) {
|
override fun onNotificationRemoved(sbn: StatusBarNotification) {
|
||||||
val notificationId = generateNotificationId(sbn)
|
val notificationId = generateNotificationId(sbn)
|
||||||
activeNotifications.remove(notificationId)
|
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?) {
|
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 {
|
val sbn = activeNotifications[notificationId] ?: run {
|
||||||
Log.w(tag, "Notification not found: $notificationId")
|
Log.w(tag, "Notification not found: $notificationId")
|
||||||
|
DebugClient.error("Notification not found for action", null, mapOf("id" to notificationId))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,18 +207,23 @@ class NotificationListener : NotificationListenerService() {
|
||||||
val action = actions.find { it.title.toString().equals(actionTitle, ignoreCase = true) }
|
val action = actions.find { it.title.toString().equals(actionTitle, ignoreCase = true) }
|
||||||
if (action == null) {
|
if (action == null) {
|
||||||
Log.w(tag, "Action not found: $actionTitle")
|
Log.w(tag, "Action not found: $actionTitle")
|
||||||
|
DebugClient.error("Action not found", null, mapOf("action" to actionTitle))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (replyText != null && action.remoteInputs?.isNotEmpty() == true) {
|
if (replyText != null && action.remoteInputs?.isNotEmpty() == true) {
|
||||||
// This is a reply action
|
|
||||||
sendReply(action, replyText)
|
sendReply(action, replyText)
|
||||||
} else {
|
} else {
|
||||||
// Regular action
|
|
||||||
action.actionIntent.send()
|
action.actionIntent.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DebugClient.event("NOTIFICATION_ACTION_TRIGGERED", mapOf(
|
||||||
|
"notificationId" to notificationId,
|
||||||
|
"action" to actionTitle,
|
||||||
|
"isReply" to (replyText != null)
|
||||||
|
))
|
||||||
|
|
||||||
ClawdNodeApp.instance.auditLog.log(
|
ClawdNodeApp.instance.auditLog.log(
|
||||||
"NOTIFICATION_ACTION",
|
"NOTIFICATION_ACTION",
|
||||||
"Triggered action: $actionTitle",
|
"Triggered action: $actionTitle",
|
||||||
|
|
@ -133,18 +231,15 @@ class NotificationListener : NotificationListenerService() {
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(tag, "Failed to trigger action", e)
|
Log.e(tag, "Failed to trigger action", e)
|
||||||
|
DebugClient.error("Action trigger failed", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissNotification(notificationId: String) {
|
fun dismissNotification(notificationId: String) {
|
||||||
|
DebugClient.log("dismissNotification called", mapOf("id" to notificationId))
|
||||||
val sbn = activeNotifications[notificationId] ?: return
|
val sbn = activeNotifications[notificationId] ?: return
|
||||||
cancelNotification(sbn.key)
|
cancelNotification(sbn.key)
|
||||||
|
DebugClient.event("NOTIFICATION_DISMISSED", mapOf("id" to notificationId))
|
||||||
ClawdNodeApp.instance.auditLog.log(
|
|
||||||
"NOTIFICATION_DISMISS",
|
|
||||||
"Dismissed notification",
|
|
||||||
mapOf("notification_id" to notificationId)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendReply(action: Notification.Action, text: String) {
|
private fun sendReply(action: Notification.Action, text: String) {
|
||||||
|
|
@ -156,6 +251,7 @@ class NotificationListener : NotificationListenerService() {
|
||||||
RemoteInput.addResultsToIntent(arrayOf(remoteInput), intent, bundle)
|
RemoteInput.addResultsToIntent(arrayOf(remoteInput), intent, bundle)
|
||||||
|
|
||||||
action.actionIntent.send(this, 0, intent)
|
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}"
|
return "${sbn.packageName}:${sbn.id}:${sbn.postTime}"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotificationEvent(sbn: StatusBarNotification, id: String): NotificationEvent {
|
private fun getAppName(packageName: String): String {
|
||||||
val notification = sbn.notification
|
return try {
|
||||||
val extras = notification.extras
|
val appInfo = packageManager.getApplicationInfo(packageName, 0)
|
||||||
|
|
||||||
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)
|
|
||||||
packageManager.getApplicationLabel(appInfo).toString()
|
packageManager.getApplicationLabel(appInfo).toString()
|
||||||
} catch (e: Exception) {
|
} 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 {
|
private fun isImportantOngoing(sbn: StatusBarNotification): Boolean {
|
||||||
// Whitelist certain ongoing notifications we care about
|
|
||||||
val importantPackages = setOf(
|
val importantPackages = setOf(
|
||||||
"com.whatsapp",
|
"com.whatsapp",
|
||||||
"org.telegram.messenger",
|
"org.telegram.messenger",
|
||||||
"com.google.android.apps.messaging"
|
"com.google.android.apps.messaging",
|
||||||
|
"org.thoughtcrime.securesms" // Signal
|
||||||
)
|
)
|
||||||
return sbn.packageName in importantPackages
|
return sbn.packageName in importantPackages
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue