feat: add GatewayService, EmbeddedBackendLifecycle, and BootReceiver for embedded flavor
Wrap GatewayProcessManager in a foreground service with pause/resume notification actions, implement BackendLifecycle for embedded flavor, and add boot-completed auto-start receiver. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
834714d700
commit
19333f6685
6 changed files with 161 additions and 0 deletions
|
|
@ -15,6 +15,9 @@ object NotificationHelper {
|
|||
const val ASSISTANT_CHANNEL_ID = "clawdroid_assistant"
|
||||
private const val ASSISTANT_CHANNEL_NAME = "Assistant"
|
||||
|
||||
const val GATEWAY_CHANNEL_ID = "clawdroid_gateway"
|
||||
private const val GATEWAY_CHANNEL_NAME = "Gateway"
|
||||
|
||||
fun createNotificationChannel(context: Context) {
|
||||
val manager = context.getSystemService(NotificationManager::class.java)
|
||||
|
||||
|
|
@ -36,6 +39,16 @@ object NotificationHelper {
|
|||
setShowBadge(false)
|
||||
}
|
||||
manager.createNotificationChannel(assistantChannel)
|
||||
|
||||
val gatewayChannel = NotificationChannel(
|
||||
GATEWAY_CHANNEL_ID,
|
||||
GATEWAY_CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply {
|
||||
description = "Gateway backend service"
|
||||
setShowBadge(false)
|
||||
}
|
||||
manager.createNotificationChannel(gatewayChannel)
|
||||
}
|
||||
|
||||
fun showMessageNotification(context: Context, content: String) {
|
||||
|
|
|
|||
|
|
@ -15,4 +15,7 @@ android {
|
|||
dependencies {
|
||||
implementation(project(":backend:api"))
|
||||
implementation(libs.coroutines.android)
|
||||
implementation(libs.lifecycle.service)
|
||||
implementation(libs.core.ktx)
|
||||
implementation(libs.koin.android)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<application>
|
||||
<service
|
||||
|
|
@ -11,5 +12,13 @@
|
|||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Local AI agent backend process" />
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".BootReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package io.clawdroid.backend.loader
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
||||
class BootReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
|
||||
val serviceIntent = Intent(context, GatewayService::class.java)
|
||||
context.startForegroundService(serviceIntent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package io.clawdroid.backend.loader
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import io.clawdroid.backend.api.BackendLifecycle
|
||||
import io.clawdroid.backend.api.BackendState
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
class EmbeddedBackendLifecycle(
|
||||
private val context: Context,
|
||||
private val processManager: GatewayProcessManager,
|
||||
) : BackendLifecycle {
|
||||
|
||||
override val isManaged: Boolean = true
|
||||
|
||||
override val state: StateFlow<BackendState> = processManager.state
|
||||
|
||||
override suspend fun start() {
|
||||
val intent = Intent(context, GatewayService::class.java)
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
|
||||
override suspend fun stop() {
|
||||
context.stopService(Intent(context, GatewayService::class.java))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package io.clawdroid.backend.loader
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class GatewayService : LifecycleService() {
|
||||
|
||||
private val processManager: GatewayProcessManager by inject()
|
||||
private lateinit var serviceScope: CoroutineScope
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
|
||||
when (intent?.action) {
|
||||
ACTION_PAUSE -> {
|
||||
serviceScope.launch {
|
||||
processManager.stop()
|
||||
updateNotification(running = false)
|
||||
}
|
||||
}
|
||||
ACTION_RESUME -> {
|
||||
serviceScope.launch {
|
||||
processManager.start()
|
||||
updateNotification(running = true)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
startForeground(
|
||||
NOTIFICATION_ID,
|
||||
buildNotification(running = true),
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
)
|
||||
serviceScope.launch { processManager.start() }
|
||||
}
|
||||
}
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
runBlocking { processManager.stop() }
|
||||
serviceScope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun updateNotification(running: Boolean) {
|
||||
val manager = getSystemService(NOTIFICATION_SERVICE) as android.app.NotificationManager
|
||||
manager.notify(NOTIFICATION_ID, buildNotification(running))
|
||||
}
|
||||
|
||||
private fun buildNotification(running: Boolean): Notification {
|
||||
val actionIntent = Intent(this, GatewayService::class.java).apply {
|
||||
action = if (running) ACTION_PAUSE else ACTION_RESUME
|
||||
}
|
||||
val pendingIntent = PendingIntent.getService(
|
||||
this, 0, actionIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val actionLabel = if (running) "Pause" else "Resume"
|
||||
val actionIcon = if (running) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play
|
||||
val contentText = if (running) "Backend running" else "Backend paused"
|
||||
|
||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
||||
.setContentTitle("ClawDroid")
|
||||
.setContentText(contentText)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setOngoing(true)
|
||||
.addAction(actionIcon, actionLabel, pendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 2002
|
||||
private const val CHANNEL_ID = "clawdroid_gateway"
|
||||
const val ACTION_PAUSE = "io.clawdroid.backend.loader.ACTION_PAUSE"
|
||||
const val ACTION_RESUME = "io.clawdroid.backend.loader.ACTION_RESUME"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue