@@ -35,7 +35,14 @@ object SessionNotifications {
3535
3636 private const val NOTIF_ID_PREFIX = 4000
3737 private const val TAG = "SessionNotifications"
38− private const val UNREAD_BADGE_ID = NOTIF_ID_PREFIX + 9999
38+ // Fixed notification IDs use NEGATIVE space (mirroring the SUMMARY_*_ID constants below)
39+ // so they can never collide with the hash-derived per-session IDs returned by notifId(),
40+ // which are always positive (NOTIF_ID_PREFIX + (hash % (0x7FFFFFFF - NOTIF_ID_PREFIX)),
41+ // range [4000, 2147483646]). A collision would let a session-specific notification
42+ // overwrite or be canceled by the fixed one (~1 in 2 billion per session, but a real
43+ // ID-space flaw). Negative IDs are valid for NotificationManager.cancel/notify.
44+ private const val UNREAD_BADGE_ID = -10
45+ private const val CONNECTION_LOST_ID = -11
3946
4047 // Distinct namespaces so a session can have a completion, a permission, and an error
4148 // notification outstanding at once without their ids colliding.
@@ -395,7 +402,7 @@ object SessionNotifications {
395402 @SuppressLint("MissingPermission")
396403 fun postConnectionLost(context: Context, profileLabel: String, profileId: String?) {
397404 if (!canPost(context)) return
398− val notifId = NOTIF_ID_PREFIX + 7000
405+ val notifId = CONNECTION_LOST_ID
399406 // Tap reopens the app at the server list so the user can reconnect / fix credentials.
400407 val openIntent = Intent(context, MainActivity::class.java).apply {
401408 flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
@@ -421,7 +428,7 @@ object SessionNotifications {
421428
422429 /** Cancel a previously-posted [postConnectionLost] notification (e.g. on reconnect). */
423430 fun cancelConnectionLost(context: Context) {
424− NotificationManagerCompat.from(context).cancel(NOTIF_ID_PREFIX + 7000)
431+ NotificationManagerCompat.from(context).cancel(CONNECTION_LOST_ID)
425432 }
426433
427434 /** FLAG_MUTABLE where required (Android 12+) so RemoteInput results can be injected. */
@@ -463,9 +470,16 @@ object SessionNotifications {
463470 .onFailure { Log.w(TAG, "Failed to post unread badge notification", it) }
464471 }
465472
466− /** Cancel a session's permission notification (on reply, or when the user opens it). */
473+ /** Cancel a session's permission notification (on reply, or when the user opens it). Also
474+ * cancels the auto-rejected-permission notification (posted under notifId + 1) so opening
475+ * the session by any route other than tapping that notification clears it too — without
476+ * this a stale "auto-rejected" heads-up lingers in the shade (setAutoCancel only dismisses
477+ * on a direct tap). */
467478 fun cancelPermission(context: Context, sessionId: String) {
468− NotificationManagerCompat.from(context).cancel(notifId(NS_PERMISSION, sessionId))
479+ val nm = NotificationManagerCompat.from(context)
480+ val baseId = notifId(NS_PERMISSION, sessionId)
481+ nm.cancel(baseId)
482+ nm.cancel(baseId + 1) // postPermissionAutoRejected uses baseId + 1
469483 maybeCancelSummary(context, GROUP_PERMISSION, SUMMARY_PERMISSION_ID)
470484 }
471485