File size: 6,641 Bytes
38cf36c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | package com.blaze.agent.task
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.util.Log
import androidx.core.app.NotificationCompat
import com.blaze.agent.ai.LLMRouter
import com.blaze.agent.state.AppState
import com.blaze.agent.state.AppStateManager
import kotlinx.coroutines.*
import org.json.JSONObject
class BackgroundTaskManager(
private val context: Context,
private val llmRouter: LLMRouter,
private val getScreenContent: () -> String,
private val executeAgentAction: suspend (String) -> Unit
) {
companion object {
private const val TAG = "BlazeTaskManager"
private const val CHANNEL_ID = "blaze_monitoring"
private const val NOTIFICATION_ID = 1001
}
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private val tasks = mutableMapOf<String, Pair<MonitoringTask, Job>>()
init { createNotificationChannel() }
fun startTask(task: MonitoringTask) {
if (tasks.containsKey(task.id)) return
Log.d(TAG, "Starting monitoring task: ${task.description}")
val job = scope.launch { runMonitoringLoop(task) }
tasks[task.id] = Pair(task, job)
AppStateManager.transitionTo(AppState.MONITORING)
updateNotification()
}
fun stopTask(taskId: String) {
val (task, job) = tasks[taskId] ?: return
job.cancel()
task.status = TaskStatus.STOPPED
tasks.remove(taskId)
handleTasksExhausted()
}
fun stopAllTasks() {
tasks.values.forEach { (task, job) -> job.cancel(); task.status = TaskStatus.STOPPED }
tasks.clear()
handleTasksExhausted()
}
fun getActiveTasks(): List<MonitoringTask> = tasks.values.map { it.first }.filter { it.isAlive }
fun hasActiveTasks(): Boolean = tasks.isNotEmpty()
private suspend fun runMonitoringLoop(task: MonitoringTask) {
while (task.status == TaskStatus.RUNNING) {
try {
delay(task.checkIntervalMs)
if (task.status != TaskStatus.RUNNING) break
task.markChecked()
val result = checkCondition(task)
if (result.conditionMet) {
task.markTriggered()
withContext(Dispatchers.Main) { AppStateManager.transitionTo(AppState.ACTIVE) }
executeAgentAction(buildActionPrompt(task, result))
withContext(Dispatchers.Main) { AppStateManager.transitionTo(AppState.MONITORING) }
if (!task.isRecurring) {
task.status = TaskStatus.COMPLETED
cleanUpTask(task.id)
break
}
}
} catch (e: CancellationException) { break
} catch (e: Exception) {
Log.e(TAG, "Error in monitoring loop for ${task.description}", e)
delay(task.checkIntervalMs)
}
}
}
private suspend fun checkCondition(task: MonitoringTask): ConditionCheckResult {
val screenContent = getScreenContent()
val prompt = """
You are monitoring a user's Android screen for a specific condition.
TASK: ${task.description}
CONDITION TO WATCH FOR: ${task.triggerCondition}
SCREEN CONTENT RIGHT NOW:
$screenContent
Is the trigger condition currently visible or met on the screen?
This task has triggered ${task.triggerCount} time(s) so far.
Respond ONLY with valid JSON:
{ "conditionMet": true or false, "specificAction": "exact action or null", "reasoning": "one sentence" }
""".trimIndent()
return try {
val raw = llmRouter.complete(prompt)
parseConditionResult(raw)
} catch (e: Exception) {
Log.e(TAG, "Condition check failed", e)
ConditionCheckResult(conditionMet = false, specificAction = null)
}
}
private fun buildActionPrompt(task: MonitoringTask, result: ConditionCheckResult): String =
"Execute this action: ${result.specificAction ?: task.actionToExecute}\nContext: ${task.description}\nReason triggered: ${result.reasoning}"
private fun parseConditionResult(raw: String): ConditionCheckResult {
return try {
val json = raw.trim().removePrefix("```json").removePrefix("```").removeSuffix("```").trim()
val obj = JSONObject(json)
ConditionCheckResult(
conditionMet = obj.getBoolean("conditionMet"),
specificAction = obj.optString("specificAction").takeIf { it != "null" && it.isNotBlank() },
reasoning = obj.optString("reasoning", "")
)
} catch (e: Exception) {
ConditionCheckResult(conditionMet = false, specificAction = null)
}
}
private fun cleanUpTask(taskId: String) {
tasks[taskId]?.second?.cancel()
tasks.remove(taskId)
handleTasksExhausted()
}
private fun handleTasksExhausted() {
if (tasks.isEmpty()) {
AppStateManager.transitionTo(AppState.SLEEP)
dismissNotification()
} else updateNotification()
}
private fun createNotificationChannel() {
val channel = NotificationChannel(CHANNEL_ID, "Blaze Background Tasks", NotificationManager.IMPORTANCE_LOW).apply {
description = "Shows when Blaze is monitoring something in the background"
setShowBadge(false)
}
(context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).createNotificationChannel(channel)
}
private fun updateNotification() {
val activeCount = tasks.size
val taskNames = tasks.values.take(3).joinToString("\n") { "• ${it.first.description.take(60)}" }
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_menu_compass)
.setContentTitle("Blaze is watching ($activeCount task${if (activeCount != 1) "s" else ""})")
.setStyle(NotificationCompat.BigTextStyle().bigText(taskNames))
.setOngoing(true).setSilent(true)
.setPriority(NotificationCompat.PRIORITY_LOW).build()
(context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(NOTIFICATION_ID, notification)
}
private fun dismissNotification() {
(context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(NOTIFICATION_ID)
}
}
|