227 lines
7.0 KiB
Kotlin
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)
|
|
}
|
|
}
|