1package soy.iko.opencode.notification
3import android.app.Notification
4import android.app.Service
5import android.content.Context
6import android.content.Intent
7import android.content.pm.ServiceInfo
9import android.os.IBinder
10import android.util.Log
11import androidx.core.app.NotificationCompat
12import androidx.core.app.ServiceCompat
13import soy.iko.opencode.R
16 * A foreground service kept alive while an opencode agent is actively running. Holding a
17 * foreground priority lets the long-lived SSE `/event` subscription survive backgrounding
18 * (Doze/app-standby would otherwise choke the socket and stall streaming mid-run).
20 * The service is started when a run begins ([start]) and stopped when it goes idle
21 * ([stop]). It shows a low-importance "Agent is working…" notification in [NotificationChannels.STATUS].
22 * Started/stopped via Compose from the chat screen based on the `running` state.
24class RunForegroundService : Service() {
26 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
27 val notification = buildNotification()
28 // startForeground can throw ForegroundServiceStartNotAllowedException on
29 // Android 12+ if the app is in the background when the service starts. Wrap
30 // it so a backgrounded start (e.g. the user navigates away at the wrong
31 // moment) doesn't crash the app — the SSE stream will just continue without
32 // foreground priority and may be killed by the system sooner.
34 ServiceCompat.startForeground(
38 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
39 ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
44 }.onFailure { Log.w(TAG, "startForeground failed; running without foreground priority", it) }
45 return START_NOT_STICKY
48 private fun buildNotification(): Notification =
49 NotificationCompat.Builder(this, NotificationChannels.STATUS)
50 .setSmallIcon(R.drawable.ic_launcher_foreground)
51 .setContentTitle(getString(R.string.notif_running_title))
52 .setContentText(getString(R.string.notif_running_text))
54 .setPriority(NotificationCompat.PRIORITY_LOW)
55 .setCategory(NotificationCompat.CATEGORY_PROGRESS)
58 override fun onDestroy() {
59 // Safety net: if the process is being killed while a run is in progress,
60 // ensure the notification is removed rather than lingering.
61 ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
65 override fun onBind(intent: Intent?): IBinder? = null
68 private const val TAG = "RunForegroundService"
69 private const val NOTIF_ID = 1
71 fun start(context: Context) {
72 val intent = Intent(context, RunForegroundService::class.java)
73 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
74 context.startForegroundService(intent)
76 context.startService(intent)
80 fun stop(context: Context) {
81 context.stopService(Intent(context, RunForegroundService::class.java))
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list