1package soy.iko.opencode.notification
3import android.app.PendingIntent
4import android.content.Context
5import android.content.Intent
8import androidx.core.app.NotificationCompat
9import androidx.core.app.NotificationManagerCompat
10import soy.iko.opencode.MainActivity
11import soy.iko.opencode.R
12import java.security.MessageDigest
15 * Builds and posts the "session completed" notification when a background session
16 * finishes a run. Tapping it opens the session via [MainActivity] with an
17 * [MainActivity.EXTRA_SESSION_ID] extra.
19 * Notification posting is guarded for Android 13+ runtime permission: if the user
20 * hasn't granted POST_NOTIFICATIONS, the post is silently skipped rather than throwing.
22object SessionNotifications {
24 private const val NOTIF_ID_PREFIX = 4000
25 private const val TAG = "SessionNotifications"
27 /** Derive a stable, collision-resistant notification id from a session id.
28 * String.hashCode() can collide for different inputs; a SHA-256 truncated to 31
29 * bits makes accidental collisions astronomically unlikely. */
30 private fun notifIdFor(sessionId: String): Int {
31 val digest = MessageDigest.getInstance("SHA-256").digest(sessionId.toByteArray())
32 // Take the first 4 bytes, mask to 31 bits (always positive) to fit an Int id.
33 return (NOTIF_ID_PREFIX + ((digest[0].toInt() and 0xFF) shl 24 or
34 (digest[1].toInt() and 0xFF) shl 16 or
35 (digest[2].toInt() and 0xFF) shl 8 or
36 (digest[3].toInt() and 0xFF))).and(0x7FFFFFFF)
39 fun postCompleted(context: Context, sessionId: String, title: String) {
40 // Respect the POST_NOTIFICATIONS runtime permission (Android 13+): if the user
41 // denied it, skip the post instead of throwing. Foreground-service notifications
42 // are exempt, so the running indicator still shows during a run.
43 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
44 androidx.core.content.ContextCompat.checkSelfPermission(
45 context, android.Manifest.permission.POST_NOTIFICATIONS,
46 ) != android.content.pm.PackageManager.PERMISSION_GRANTED
50 val notifId = notifIdFor(sessionId)
51 val openIntent = Intent(context, MainActivity::class.java).apply {
52 action = Intent.ACTION_VIEW
53 flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
54 putExtra(MainActivity.EXTRA_SESSION_ID, sessionId)
56 val pendingIntent = PendingIntent.getActivity(
57 context, notifId, openIntent,
58 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
60 val notification = NotificationCompat.Builder(context, NotificationChannels.COMPLETED)
61 .setSmallIcon(R.drawable.ic_launcher_foreground)
62 .setContentTitle(context.getString(R.string.notif_completed_title))
63 .setContentText(context.getString(R.string.notif_completed_text, title))
65 .setContentIntent(pendingIntent)
66 .setCategory(NotificationCompat.CATEGORY_MESSAGE)
67 .setPriority(NotificationCompat.PRIORITY_DEFAULT)
70 runCatching { NotificationManagerCompat.from(context).notify(notifId, notification) }
71 .onFailure { Log.w(TAG, "Failed to post completion notification", it) }
74 /** Cancel a session's completion notification (e.g. when the user opens it). */
75 fun cancel(context: Context, sessionId: String) {
76 val notifId = notifIdFor(sessionId)
77 NotificationManagerCompat.from(context).cancel(notifId)
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