_reconcileElapsedTime() runs off a Dart Stopwatch, which is CLOCK_MONOTONIC
and does not advance while the device is suspended - exactly the state an
RTC_WAKEUP expiry alarm fires in. The "did this drop happen at the deadline"
test therefore came out false after any sleep, and the screen-off expiry (the
common one) still showed no reason in the UI.
Each successful sync now records an absolute DateTime deadline, and the
tunnel-dropped branch compares against that instead.
Co-Authored-By: Claude Opus 5 <[email protected]>
Review of 742f376 (Fable). Three defects made the feature not work for the
case it exists for:
- armExpiryWarning() cancelled the alarm before its own early return. Dart
re-pushes the deadline every 60 s with no dedupe (rule 93's ">5 s" dedupe
does not exist), so the first sync after the warning came due deleted the
armed alarm and never re-armed it. Cancel now happens only on the branch
that re-arms.
- cancelSessionWarningAlarm() built its PendingIntent with
FLAG_UPDATE_CURRENT, rewriting the live alarm's RUNTIME_TOKEN to "" so a
delivery in flight was dropped as a stale generation. It now looks the
existing PendingIntent up with FLAG_NO_CREATE.
- SOCKS5 mode never requested POST_NOTIFICATIONS (requestPermission() is
TUN-only because it also raises the VPN consent dialog), so on Android 13+
both alerts were dropped. New plugin method requests it on its own.
Also:
- the native deadline wins the expiry race against the 1 s Dart tick, so the
commonest session end reached _onVpnConnectionChanged as a bare tunnel
drop with no message. It now reconciles and sets the expiry text, with
postAlert false since the native side already posted.
- a resume reconnect no longer resets the support-ad claim: it re-attaches
to the same server session, where the top-up has already been spent.
- RevoltAlerts sets setOnlyAlertOnce so a replacing post does not re-sound.
- AGENTS.md rule 93 described a setSessionExpiry/SESSION_EXPIRES_AT_MS path
that no longer exists, dedupe included. Corrected; that stale text is what
the warning bug was written against.
- CHANGELOG no longer claims the alert covers the tunnel stopping on its own.
flutter analyze clean; debug APK builds.
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]>
With ads enabled the SSV nonce and the nonce the connect flow waited for
were minted separately, so the client rejected the very session the ad had
just created and failed with "Session not activated".
- showAd() returns the nonce it put in the SSV custom_data (null on no reward)
- fetchConfigDirectly({adNonce, resume}) replaces skipAdBypass: a real ad
waits for exactly that nonce and polls ~36 s for Google's callback, the
bypass path mints its own, and resume re-attaches to the session the
device already owns
- SOCKS5 "Reconnect" uses resume: true instead of requesting a new session
Co-Authored-By: Claude Opus 5 <[email protected]>
- fix(app): Test Local SOCKS forces a round trip instead of trusting the CONNECT reply
- changelog: notification icon + SOCKS test fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The migration write is best-effort again. `_mode` is already the safe TUN
default in memory by the time it runs, so a failed write leaves nothing
insecure -- it only means the migration is retried on the next launch.
Throwing there was a startup brick rather than a fail-closed guard.
`initialize()` is awaited from `main()` at lib/main.dart:19, before
`runApp()`, with no try/catch and no `runZonedGuarded`; `FlutterError.onError`
and `PlatformDispatcher.onError` are installed later and would not catch it
either. The branch also runs on every fresh install, since `storedMode` is null
and `null != 'tun'`. A `setString` returning false would therefore have meant a
black screen with no error UI, repeating every launch, in exchange for no
security benefit.
Also restores two comments this PR removed from code it does not otherwise
touch -- the `_reconnect` note on why the repair path shows no rewarded ad, and
the explanation of how an orphaned proxy outlives the app process -- and drops
a stray 'ass' from the legacy-values comment.
The `setMode` half of the PR is unchanged: persisting before mutating memory
and surfacing failures to the UI is the right behaviour.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019HT2x2JvJNg7XNsJubfHFf
#17 removed the `setSessionExpiry` and `isVpnRuntimeAlive` MethodChannel
handlers from MainActivity, and #18 removed their last callers. The Dart
wrappers stayed behind.
Both swallow their errors, so a future caller would not fail loudly — it would
get a silent no-op from `setSessionExpiry` and a permanent
`processAlive: false` from `runtimeState()`, which previously read as "the
tunnel is dead". Deleting them keeps that trap out of the codebase.
`VpnRuntimeState` goes with them; runtime liveness now arrives through the
native status stream, which is generation-scoped. The battery-optimisation
methods are untouched and still used by background_reliability.dart.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_019HT2x2JvJNg7XNsJubfHFf
Remove UI and provider state that does not control the authoritative tunnel destination while preserving the existing connection-health status behavior.
Enforce session expiry at the OS level (AlarmManager + handler) so the tunnel dies even if the UI process is killed; persist the session so a START_STICKY restart re-establishes it; sync immediately after a support top-up; slow the session beacon 5s/30s -> 60s/60s; drop the Always-on-VPN deep-link and the legacy 'ass' migration. Docs: CHANGELOG 3.3.4, privacy-policy log disclosure, README.
The clock sat at 00:00:00 on a connected tunnel, and the notification
countdown froze with it.
SessionTimer is built lazily, so when the app reattaches to a tunnel that
outlived its UI process, VpnConnection had already reported CONNECTED
before the timer existed. A change listener does not replay events it was
not present for, so the timer never learned it should be running. The
notification text is pushed by that same timer, which is why the countdown
froze at the same moment -- one cause, both symptoms.
The timer now evaluates the current connection state once at construction
rather than waiting for a transition, and is created eagerly alongside
VpnConnection so it is listening from the start.
Resuming also no longer requires adoptedRunningRuntime or a previous
successful sync. Both are false in a freshly started process, which is
exactly the case that needed to work. A connected tunnel with a stopped
clock is now always resumed.
Version 3.3.3+29.
Co-Authored-By: Claude Opus 5 <[email protected]>
3.3.1 could display "connected" against a tunnel that was no longer
running, with a frozen countdown and no traffic. Three causes, and the
fix for each removes more code than it adds.
Liveness was read from getConnectedServerDelay(), which measures through
AppConfigs.V2RAY_CONFIG?.LOCAL_SOCKS5_PORT ?: 10807. AppConfigs lives in
the :RunSoLibXrayDaemon process, so from the UI process it is always null
and the port always fell back to 10807 -- accidentally correct while the
SOCKS port was fixed, permanently wrong once 3.3.1 made it ephemeral.
Liveness now comes from the OS: is our service process running, and is a
VPN transport actually present.
The foreground notification (id 1) is owned by XrayVPNService via
startForeground, but MainActivity was posting to the same id from the UI
process. When that process died its notification stayed on screen with
nothing left to update it. MainActivity now refuses to post for a dead
runtime and clears the stale notification instead.
Nothing ever asked Android for permission to stay alive. Adds the
battery-optimisation exemption prompt and a deep link to Always-on VPN --
the two sanctioned mechanisms for background survival, each needing one
user tap.
Removes Standard/Extreme resilience and the runtime-restart machinery:
recovery ran off a disconnect broadcast delivered to the app process, so
it could never fire when Android killed that process. Removes startup
restoration and the config-snapshot rebuild along with it; with the OS
keeping the tunnel up there is nothing to restore.
Version 3.3.2+28.
Co-Authored-By: Claude Opus 5 <[email protected]>
Maintainer changes on top of PR #2 (Naveax). Reduces the release to two
deliberate connection modes and resolves the issues found in review:
- Lock-screen privacy: move VISIBILITY_PRIVATE and a generic publicVersion
into MainActivity.updateVpnNotification. The PR's fix lived in an
unreferenced class and never reached the live notification.
- Haptics: restore the default to off, and reduce HapticSettings to a
single success() on connect/disconnect via the native channel.
- SOCKS5: make it a real proxyOnly local-proxy mode rather than a TUN
connection under a different label. TUN stays the default.
- Health probe: restore the idle-only guard so /health is no longer polled
every 30s while connected.
- Recovery: add a _connectionEpoch counter so connect() aborts an
in-flight Extreme recovery.
- Startup restoration: rebuild the recovery snapshot from the still-active
session after an app-process restart (TUN), and surface an honest
orphaned state with a Reconnect action in proxy mode.
Drops per-app routing entirely. It only ever functioned in TUN --
addDisallowedApplication lives inside the VPN-interface setup that
proxyOnly skips -- and its allowlist half had no Dart caller.
Also removes flutter_local_notifications, the global tap-haptic surface,
and unused runtime telemetry.
Co-Authored-By: Claude Opus 5 <[email protected]>
Keep the ongoing VPN notification visible while marking its timer and speed content private so Android does not expose session metadata in public lock-screen previews.
Add explicit TUN and SOCKS5 connection modes, app routing controls, authenticated per-session local SOCKS5, Android network monitoring, persistent haptics, resilient session recovery, notification hardening and client-side Hivemind cleanup.
Keep the hosted flutter_vless_android 1.1.5 runtime as the base and apply the Android compatibility/security patches at build time instead of restoring the unstable vendored runtime.
User-Agent spoofed to Chrome on Android (was Dart/3.x). Domain renamed to userevolt.app. Branding to Revolt VPN. Removed duplicate notification, patched flutter_vless for custom icon + dark background. Merged 5 disconnect paths into single idempotent gate, fixed stopVless timeout silently lying about state. Removed kDebugMode guard from AdMob bypass.