1package soy.iko.opencode.notification
3import android.app.Notification
4import android.app.PendingIntent
5import android.app.Service
6import android.content.Context
7import android.content.Intent
8import android.content.pm.ServiceInfo
10import android.os.IBinder
11import android.util.Log
12import androidx.core.app.NotificationCompat
13import androidx.core.app.ServiceCompat
14import soy.iko.opencode.MainActivity
15import soy.iko.opencode.R
18 * A foreground service kept alive while an opencode agent is actively running. Holding a
19 * foreground priority lets the long-lived SSE `/event` subscription survive backgrounding
20 * (Doze/app-standby would otherwise choke the socket and stall streaming mid-run).
22 * The service is started when a run begins ([start]) and stopped when it goes idle
23 * ([stop]). It shows a low-importance "Agent is working…" notification in [NotificationChannels.STATUS].
24 * Started/stopped via Compose from the chat screen based on the `running` state.
26class RunForegroundService : Service() {
28 override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
29 val notification = buildNotification()
30 // startForeground can throw ForegroundServiceStartNotAllowedException on
31 // Android 12+ if the app is in the background when the service starts. Wrap
32 // it so a backgrounded start (e.g. the user navigates away at the wrong
33 // moment) doesn't crash the app — the SSE stream will just continue without
34 // foreground priority and may be killed by the system sooner.
36 ServiceCompat.startForeground(
40 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
41 ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
47 Log.w(TAG, "startForeground failed; running without foreground priority", it)
50 return START_NOT_STICKY
53 private fun buildNotification(): Notification {
54 // Tapping the notification opens the app so the user can see the running session.
55 val openIntent = Intent(this, MainActivity::class.java).apply {
56 flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
58 val pendingIntent = PendingIntent.getActivity(
60 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
62 return NotificationCompat.Builder(this, NotificationChannels.STATUS)
63 .setSmallIcon(R.drawable.ic_launcher_foreground)
64 .setContentTitle(getString(R.string.notif_running_title))
65 .setContentText(getString(R.string.notif_running_text))
66 .setContentIntent(pendingIntent)
68 .setPriority(NotificationCompat.PRIORITY_LOW)
69 .setCategory(NotificationCompat.CATEGORY_PROGRESS)
73 override fun onDestroy() {
74 // Safety net: if the process is being killed while a run is in progress,
75 // ensure the notification is removed rather than lingering.
76 ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
80 override fun onBind(intent: Intent?): IBinder? = null
83 private const val TAG = "RunForegroundService"
84 private const val NOTIF_ID = 1
86 fun start(context: Context) {
87 // startForegroundService can throw ForegroundServiceStartNotAllowedException
88 // on Android 12+ if the app is in the background. Wrap it so a backgrounded
89 // start (e.g. the user navigates away at the wrong moment) doesn't crash.
91 context.startForegroundService(Intent(context, RunForegroundService::class.java))
93 Log.w(TAG, "startForegroundService failed; running without foreground priority", it)
97 fun stop(context: Context) {
98 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