Disconnecting the VPN from Android's own settings left "Revolt VPN
00:28:13" in the shade, frozen, for as long as the phone stayed up - a
notification claiming a protected session over a tunnel that was gone.
updateTimer() re-posts notification id 1 as ongoing and non-dismissible
from the UI process, every tick. The service cancels its own foreground
copy when it stops, but a tick landing just after that re-creates it as
an ordinary notification nobody owns, and nothing ever cancelled it:
NotificationService.reset() only clears the dedupe cache. AGENTS.md
rule 31 describes the cancel this restores.
_doDisconnect now cancels it, after vpnConnection.disconnect() so the
service is already down - cancelling while it is still in the foreground
would be swallowed by the platform.
Verified on an emulator, revoking via Settings -> VPN -> Disconnect:
before countdown stranded at 00:28:13 indefinitely
after no live notifications
User disconnect still posts no alert, and expiry still posts "Revolt VPN
disconnected" with the countdown gone.
Co-Authored-By: Claude Opus 5 <[email protected]>
Review findings #1-#3 in internal/APP_REVIEW_2026-10-16.md: every session
end dropped the user onto their real IP with no warning, and the app showed
no errors at all.
- RevoltAlerts (vendored runtime) owns both alerts: a high-importance
channel, ids 2 and 3, posted from whichever process is alive
- XrayVPNService schedules a 5-minute warning off the deadline it already
holds, best effort (a warning that cannot be armed never stops a tunnel),
with its own request code so it cannot clobber the expiry alarm
- the warning intent is handled above the command dispatch: without that it
would fall through to the final else and stop the tunnel 5 minutes early
- SessionTimer._doDisconnect takes endedMessage: set for ends the user did
not ask for, which both posts the alert and gives the UI a sentence
- the API being unreachable no longer disconnects a working tunnel; it is a
display state now, with expiry still enforced natively and server-side
- status_bar.dart renders connect errors, connecting progress and the
session-end reason
flutter analyze clean; debug APK builds (Kotlin compiles).
Co-Authored-By: Claude Opus 5 <[email protected]>