clawdnode-android/app/src/main/java/com/inou/clawdnode/service/NodeService.kt

227 lines
7.0 KiB
Kotlin

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)
}
}