diff --git a/app/src/main/java/com/inou/clawdnode/ClawdNodeApp.kt b/app/src/main/java/com/inou/clawdnode/ClawdNodeApp.kt index 43313c5..af98a0d 100644 --- a/app/src/main/java/com/inou/clawdnode/ClawdNodeApp.kt +++ b/app/src/main/java/com/inou/clawdnode/ClawdNodeApp.kt @@ -9,6 +9,7 @@ import com.inou.clawdnode.gateway.DirectGateway import com.inou.clawdnode.security.AuditLog import com.inou.clawdnode.security.DeviceIdentity import com.inou.clawdnode.security.TokenStore +import com.inou.clawdnode.service.CallManager import com.inou.clawdnode.service.NotificationManager as AppNotificationManager /** @@ -58,17 +59,17 @@ class ClawdNodeApp : Application() { DirectGateway.onCallAnswer = { callId -> auditLog.log("COMMAND_RECEIVED", "call.answer: $callId") - // TODO: Implement call answering via TelecomManager + CallManager.answer(callId, null) } DirectGateway.onCallReject = { callId -> auditLog.log("COMMAND_RECEIVED", "call.reject: $callId") - // TODO: Implement call rejection + CallManager.reject(callId, null) } DirectGateway.onCallHangup = { callId -> auditLog.log("COMMAND_RECEIVED", "call.hangup: $callId") - // TODO: Implement call hangup + CallManager.hangup(callId) } // Connect 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 c29498f..0b85022 100644 --- a/app/src/main/java/com/inou/clawdnode/calls/CallScreener.kt +++ b/app/src/main/java/com/inou/clawdnode/calls/CallScreener.kt @@ -4,7 +4,10 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection +import android.database.Cursor +import android.net.Uri import android.os.IBinder +import android.provider.ContactsContract import android.telecom.Call import android.telecom.CallScreeningService import android.util.Log @@ -104,9 +107,26 @@ class CallScreener : CallScreeningService() { } private fun lookupContact(number: String): String? { - // TODO: Look up in contacts - // For now, return null (unknown caller) - return null + return try { + val uri = Uri.withAppendedPath( + 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 + } } } diff --git a/app/src/main/java/com/inou/clawdnode/calls/VoiceCallService.kt b/app/src/main/java/com/inou/clawdnode/calls/VoiceCallService.kt index 273ae7d..d5a33b2 100644 --- a/app/src/main/java/com/inou/clawdnode/calls/VoiceCallService.kt +++ b/app/src/main/java/com/inou/clawdnode/calls/VoiceCallService.kt @@ -40,6 +40,7 @@ class VoiceCallService : InCallService(), TextToSpeech.OnInitListener { private var audioManager: AudioManager? = null private val activeCalls = mutableMapOf() + private val callStartTimes = mutableMapOf() private var currentCallId: String? = null private var isListening = false @@ -135,25 +136,32 @@ class VoiceCallService : InCallService(), TextToSpeech.OnInitListener { when (state) { Call.STATE_ACTIVE -> { - // Call is active, start listening + // Call is active, record start time and start listening + callStartTimes[callId] = System.currentTimeMillis() currentCallId = callId startListening() } Call.STATE_DISCONNECTED -> { - // Call ended - val duration = 0 // TODO: Calculate actual duration - DirectGateway.sendCall( - callId = callId, - number = call.details.handle?.schemeSpecificPart, - contact = null, - state = "ended" - ) + // Call ended - calculate duration + val startTime = callStartTimes.remove(callId) + val duration = if (startTime != null) { + ((System.currentTimeMillis() - startTime) / 1000).toInt() + } else { + 0 + } + + 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( "CALL_ENDED", call.details.handle?.schemeSpecificPart, null, - "completed" + "completed (${duration}s)" ) } } diff --git a/app/src/main/java/com/inou/clawdnode/service/GatewayClient.kt b/app/src/main/java/com/inou/clawdnode/service/GatewayClient.kt index d5fcb82..891bf8f 100644 --- a/app/src/main/java/com/inou/clawdnode/service/GatewayClient.kt +++ b/app/src/main/java/com/inou/clawdnode/service/GatewayClient.kt @@ -62,6 +62,10 @@ class GatewayClient( private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val requestIdCounter = AtomicInteger(0) + // Queue for events when disconnected (max 100 to prevent memory issues) + private val eventQueue = mutableListOf() + private val maxQueueSize = 100 + private val auditLog get() = ClawdNodeApp.instance.auditLog private val tokenStore get() = ClawdNodeApp.instance.tokenStore @@ -146,8 +150,33 @@ class GatewayClient( if (isConnected && isHandshakeComplete) { webSocket?.send(json) } else { - log("Not connected or handshake incomplete, cannot send event") - // TODO: Queue for retry + // Queue for retry when reconnected + 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 + 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 onConnectionChange(true) auditLog.log("GATEWAY_CONNECTED", "Protocol v$protocol handshake complete") + + // Send any queued events + flushEventQueue() } private fun handleDisconnect() {