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:
parent
b7e213ff32
commit
0cd111343f
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue