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 // Show a live elapsed timer so the user can see how long the current run has
69 // been going — no per-tick plumbing needed; the system renders the chronometer.
70 .setWhen(System.currentTimeMillis())
71 .setUsesChronometer(true)
73 .setPriority(NotificationCompat.PRIORITY_LOW)
74 .setCategory(NotificationCompat.CATEGORY_PROGRESS)
78 // Android 14+ can time out a foreground service (notably the ~6h/day cumulative cap on
79 // dataSync in Android 15) and calls onTimeout expecting a prompt stop; not stopping risks
80 // the system force-stopping/ANR-ing the app. Stop cleanly — a still-active run just
81 // continues without foreground priority. Both overloads are covered (the 2-arg form is
83 override fun onTimeout(startId: Int) {
84 Log.w(TAG, "Foreground service timed out; stopping")
88 override fun onTimeout(startId: Int, fgsType: Int) {
89 Log.w(TAG, "Foreground service timed out (type=$fgsType); stopping")
93 override fun onDestroy() {
94 // Safety net: if the process is being killed while a run is in progress,
95 // ensure the notification is removed rather than lingering.
96 ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
100 override fun onBind(intent: Intent?): IBinder? = null
103 private const val TAG = "RunForegroundService"
104 private const val NOTIF_ID = 1
106 fun start(context: Context) {
107 // startForegroundService can throw ForegroundServiceStartNotAllowedException
108 // on Android 12+ if the app is in the background. Wrap it so a backgrounded
109 // start (e.g. the user navigates away at the wrong moment) doesn't crash.
111 context.startForegroundService(Intent(context, RunForegroundService::class.java))
113 Log.w(TAG, "startForegroundService failed; running without foreground priority", it)
117 fun stop(context: Context) {
118 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