fix: implement TODOs for call duration, contact lookup, and event queue

- VoiceCallService: Track call start time and calculate actual duration on disconnect
- CallScreener: Implement contact lookup via ContactsContract.PhoneLookup
- GatewayClient: Add event queue (max 100) for offline buffering, flush on reconnect
- ClawdNodeApp: Route call handlers through CallManager instead of TODOs
This commit is contained in:
James (ClawdBot) 2026-03-02 01:04:30 +00:00
parent b7e213ff32
commit 0cd111343f
4 changed files with 79 additions and 18 deletions

View File

@ -9,6 +9,7 @@ import com.inou.clawdnode.gateway.DirectGateway
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
import com.inou.clawdnode.service.CallManager
import com.inou.clawdnode.service.NotificationManager as AppNotificationManager import com.inou.clawdnode.service.NotificationManager as AppNotificationManager
/** /**
@ -58,17 +59,17 @@ class ClawdNodeApp : Application() {
DirectGateway.onCallAnswer = { callId -> DirectGateway.onCallAnswer = { callId ->
auditLog.log("COMMAND_RECEIVED", "call.answer: $callId") auditLog.log("COMMAND_RECEIVED", "call.answer: $callId")
// TODO: Implement call answering via TelecomManager CallManager.answer(callId, null)
} }
DirectGateway.onCallReject = { callId -> DirectGateway.onCallReject = { callId ->
auditLog.log("COMMAND_RECEIVED", "call.reject: $callId") auditLog.log("COMMAND_RECEIVED", "call.reject: $callId")
// TODO: Implement call rejection CallManager.reject(callId, null)
} }
DirectGateway.onCallHangup = { callId -> DirectGateway.onCallHangup = { callId ->
auditLog.log("COMMAND_RECEIVED", "call.hangup: $callId") auditLog.log("COMMAND_RECEIVED", "call.hangup: $callId")
// TODO: Implement call hangup CallManager.hangup(callId)
} }
// Connect // Connect

View File

@ -4,7 +4,10 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.database.Cursor
import android.net.Uri
import android.os.IBinder import android.os.IBinder
import android.provider.ContactsContract
import android.telecom.Call import android.telecom.Call
import android.telecom.CallScreeningService import android.telecom.CallScreeningService
import android.util.Log import android.util.Log
@ -104,9 +107,26 @@ class CallScreener : CallScreeningService() {
} }
private fun lookupContact(number: String): String? { private fun lookupContact(number: String): String? {
// TODO: Look up in contacts return try {
// For now, return null (unknown caller) val uri = Uri.withAppendedPath(
return null ContactsContract.PhoneLookup.CONTENT_FILTER_URI,
Uri.encode(number)
)
val projection = arrayOf(ContactsContract.PhoneLookup.DISPLAY_NAME)
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.getString(
cursor.getColumnIndexOrThrow(ContactsContract.PhoneLookup.DISPLAY_NAME)
)
} else {
null
}
}
} catch (e: Exception) {
Log.w(tag, "Contact lookup failed for $number: ${e.message}")
null
}
} }
} }

View File

@ -40,6 +40,7 @@ class VoiceCallService : InCallService(), TextToSpeech.OnInitListener {
private var audioManager: AudioManager? = null private var audioManager: AudioManager? = null
private val activeCalls = mutableMapOf<String, Call>() private val activeCalls = mutableMapOf<String, Call>()
private val callStartTimes = mutableMapOf<String, Long>()
private var currentCallId: String? = null private var currentCallId: String? = null
private var isListening = false private var isListening = false
@ -135,25 +136,32 @@ class VoiceCallService : InCallService(), TextToSpeech.OnInitListener {
when (state) { when (state) {
Call.STATE_ACTIVE -> { Call.STATE_ACTIVE -> {
// Call is active, start listening // Call is active, record start time and start listening
callStartTimes[callId] = System.currentTimeMillis()
currentCallId = callId currentCallId = callId
startListening() startListening()
} }
Call.STATE_DISCONNECTED -> { Call.STATE_DISCONNECTED -> {
// Call ended // Call ended - calculate duration
val duration = 0 // TODO: Calculate actual duration val startTime = callStartTimes.remove(callId)
DirectGateway.sendCall( val duration = if (startTime != null) {
callId = callId, ((System.currentTimeMillis() - startTime) / 1000).toInt()
number = call.details.handle?.schemeSpecificPart, } else {
contact = null, 0
state = "ended" }
)
DirectGateway.sendLog("call.ended", mapOf(
"callId" to callId,
"number" to (call.details.handle?.schemeSpecificPart ?: "unknown"),
"duration" to duration,
"outcome" to "completed"
))
ClawdNodeApp.instance.auditLog.logCall( ClawdNodeApp.instance.auditLog.logCall(
"CALL_ENDED", "CALL_ENDED",
call.details.handle?.schemeSpecificPart, call.details.handle?.schemeSpecificPart,
null, null,
"completed" "completed (${duration}s)"
) )
} }
} }

View File

@ -62,6 +62,10 @@ class GatewayClient(
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val requestIdCounter = AtomicInteger(0) private val requestIdCounter = AtomicInteger(0)
// Queue for events when disconnected (max 100 to prevent memory issues)
private val eventQueue = mutableListOf<NodeEvent>()
private val maxQueueSize = 100
private val auditLog get() = ClawdNodeApp.instance.auditLog private val auditLog get() = ClawdNodeApp.instance.auditLog
private val tokenStore get() = ClawdNodeApp.instance.tokenStore private val tokenStore get() = ClawdNodeApp.instance.tokenStore
@ -146,8 +150,33 @@ class GatewayClient(
if (isConnected && isHandshakeComplete) { if (isConnected && isHandshakeComplete) {
webSocket?.send(json) webSocket?.send(json)
} else { } else {
log("Not connected or handshake incomplete, cannot send event") // Queue for retry when reconnected
// TODO: Queue for retry synchronized(eventQueue) {
if (eventQueue.size >= maxQueueSize) {
// Drop oldest event to make room
val dropped = eventQueue.removeAt(0)
log("Queue full, dropped oldest event: ${dropped.type}")
}
eventQueue.add(event)
log("Queued event for retry (queue size: ${eventQueue.size})")
}
}
}
private fun flushEventQueue() {
val eventsToSend: List<NodeEvent>
synchronized(eventQueue) {
eventsToSend = eventQueue.toList()
eventQueue.clear()
}
if (eventsToSend.isNotEmpty()) {
log("Flushing ${eventsToSend.size} queued events")
eventsToSend.forEach { event ->
val requestId = generateRequestId()
val json = event.toProtocolFrame(requestId)
webSocket?.send(json)
}
} }
} }
@ -365,6 +394,9 @@ class GatewayClient(
isHandshakeComplete = true isHandshakeComplete = true
onConnectionChange(true) onConnectionChange(true)
auditLog.log("GATEWAY_CONNECTED", "Protocol v$protocol handshake complete") auditLog.log("GATEWAY_CONNECTED", "Protocol v$protocol handshake complete")
// Send any queued events
flushEventQueue()
} }
private fun handleDisconnect() { private fun handleDisconnect() {