package com.inou.clawdnode.service import android.app.Notification import android.app.PendingIntent import android.app.Service import android.content.Intent import android.os.Binder import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat import com.inou.clawdnode.ClawdNodeApp import com.inou.clawdnode.R import com.inou.clawdnode.protocol.* import com.inou.clawdnode.ui.MainActivity /** * Main foreground service that maintains Gateway connection * and coordinates all ClawdNode functionality. */ class NodeService : Service() { private val tag = "NodeService" private val binder = LocalBinder() private lateinit var gatewayClient: GatewayClient private var isConnected = false // Callbacks for UI updates var onConnectionChange: ((Boolean) -> Unit)? = null inner class LocalBinder : Binder() { fun getService(): NodeService = this@NodeService } override fun onBind(intent: Intent?): IBinder = binder override fun onCreate() { super.onCreate() Log.i(tag, "NodeService created") gatewayClient = GatewayClient( onCommand = { command -> handleCommand(command) }, onConnectionChange = { connected -> isConnected = connected updateNotification() onConnectionChange?.invoke(connected) } ) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { Log.i(tag, "NodeService starting") // Start as foreground service startForeground(NOTIFICATION_ID, createNotification()) // Connect to gateway if (ClawdNodeApp.instance.tokenStore.isConfigured) { gatewayClient.connect() } else { Log.w(tag, "Gateway not configured, waiting for setup") } return START_STICKY } override fun onDestroy() { Log.i(tag, "NodeService destroyed") gatewayClient.disconnect() ClawdNodeApp.instance.auditLog.log("SERVICE_STOP", "NodeService destroyed") super.onDestroy() } // ======================================== // PUBLIC API // ======================================== fun connect() { gatewayClient.connect() } fun disconnect() { gatewayClient.disconnect() } fun isConnected() = isConnected fun sendEvent(event: NodeEvent) { gatewayClient.send(event) } // ======================================== // COMMAND HANDLING // ======================================== private fun handleCommand(command: NodeCommand) { Log.d(tag, "Handling command: ${command::class.simpleName}") when (command) { is ScreenshotCommand -> handleScreenshot() is NotificationActionCommand -> handleNotificationAction(command) is NotificationDismissCommand -> handleNotificationDismiss(command) is CallAnswerCommand -> handleCallAnswer(command) is CallRejectCommand -> handleCallReject(command) is CallSilenceCommand -> handleCallSilence(command) is CallSpeakCommand -> handleCallSpeak(command) is CallHangupCommand -> handleCallHangup(command) } } private fun handleScreenshot() { // TODO: Implement screenshot capture via MediaProjection Log.d(tag, "Screenshot requested - not yet implemented") } private fun handleNotificationAction(cmd: NotificationActionCommand) { // Delegate to NotificationListener NotificationManager.triggerAction(cmd.notificationId, cmd.action, cmd.text) } private fun handleNotificationDismiss(cmd: NotificationDismissCommand) { NotificationManager.dismiss(cmd.notificationId) } private fun handleCallAnswer(cmd: CallAnswerCommand) { CallManager.answer(cmd.callId, cmd.greeting) } private fun handleCallReject(cmd: CallRejectCommand) { CallManager.reject(cmd.callId, cmd.reason) } private fun handleCallSilence(cmd: CallSilenceCommand) { CallManager.silence(cmd.callId) } private fun handleCallSpeak(cmd: CallSpeakCommand) { CallManager.speak(cmd.callId, cmd.text, cmd.voice) } private fun handleCallHangup(cmd: CallHangupCommand) { CallManager.hangup(cmd.callId) } // ======================================== // NOTIFICATION // ======================================== private fun createNotification(): Notification { val intent = Intent(this, MainActivity::class.java) val pendingIntent = PendingIntent.getActivity( this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) val status = if (isConnected) "Connected to Gateway" else "Disconnected" return NotificationCompat.Builder(this, ClawdNodeApp.CHANNEL_SERVICE) .setContentTitle("ClawdNode") .setContentText(status) .setSmallIcon(R.drawable.ic_notification) .setOngoing(true) .setContentIntent(pendingIntent) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .build() } private fun updateNotification() { val notification = createNotification() val manager = getSystemService(NOTIFICATION_SERVICE) as android.app.NotificationManager manager.notify(NOTIFICATION_ID, notification) } companion object { const val NOTIFICATION_ID = 1001 } } /** * Singleton managers for coordinating with system services. * These get populated by the respective listener services. */ object NotificationManager { private var listener: com.inou.clawdnode.notifications.NotificationListener? = null fun register(listener: com.inou.clawdnode.notifications.NotificationListener) { this.listener = listener } fun triggerAction(notificationId: String, action: String, text: String?) { listener?.triggerAction(notificationId, action, text) } fun dismiss(notificationId: String) { listener?.dismissNotification(notificationId) } } object CallManager { private var callService: com.inou.clawdnode.calls.VoiceCallService? = null fun register(service: com.inou.clawdnode.calls.VoiceCallService) { this.callService = service } fun answer(callId: String, greeting: String?) { callService?.answerCall(callId, greeting) } fun reject(callId: String, reason: String?) { callService?.rejectCall(callId) } fun silence(callId: String) { callService?.silenceCall(callId) } fun speak(callId: String, text: String, voice: String?) { callService?.speakIntoCall(callId, text) } fun hangup(callId: String) { callService?.hangupCall(callId) } }