Commit Graph
10 Commits
Author SHA1 Message Date
esefxdzandClaude Opus 5 00b84dde61 fix(session): stop the expiry warning promising a time and an ad it cannot honour
The warning said "Revolt VPN disconnects in 5 minutes" / "Watch an ad to
extend your session." Both can be wrong.

handleSessionWarningAlarm accepts the warning anywhere between
WARNING_FLOOR_MS and WARNING_LEAD_MS + WARNING_SLACK_MS remaining, so it
can say "5 minutes" with just over one left. And the ad is a dead end in
a release build: AdManager.showAd('support') is still kDebugMode-gated
(rule 87), so the support button does nothing there - and nothing at all
in a build shipped with ads off.

Say neither. The comment records what has to change for the number and
the ad to come back.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 16:26:41 +03:00
esefxdzandClaude Opus 5 eab43b0420 fix(session): deliver the expiry warning when the app is closed
The five-minute warning did not arrive in the one case it exists for.

The deadline is enforced by two things - a main-looper Handler and an
alarm - while the warning had only the alarm, and that alarm is inexact
on purpose so the app can stay off SCHEDULE_EXACT_ALARM. Doze defers an
inexact alarm freely, and with the app closed it deferred this one past
the end of the session it was warning about.

Measured on an emulator, app swiped from recents and its process killed,
screen off, deviceidle forced to IDLE, 7-minute session:

  before  warning never fired; request code 1002 never delivered and
          handleSessionWarningAlarm never ran. Expiry was still punctual
          (16:00:55 against a 16:00:54 deadline) because the deadline has
          its Handler.
  after   warning posted as the session crossed 300 s remaining, app
          still closed and device still IDLE; expiry unchanged.

So give the warning the same backstop that already makes the deadline
reliable. postDelayed runs on uptimeMillis, so a genuinely suspended
device still fires it late - handleSessionWarningAlarm then drops it on
the existing window check instead of claiming "5 minutes" with seconds
left. The alarm stays for the case where the process was restarted and
no Handler callback survives.

Note this does not make the warning punctual under true suspend; only
SCHEDULE_EXACT_ALARM would, and that permission is deliberately not
declared.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 16:14:48 +03:00
esefxdzandClaude Opus 5 e7ef251882 fix(runtime): keep Xray's startup output so a failed core can be diagnosed
The monitor thread read the core's merged stdout/stderr and dropped
every line:

    process.inputStream.bufferedReader().use { reader ->
        reader.forEachLine { _ -> }
    }

so the only trace of a core that died at startup was "Xray process
exited with code 23", with nothing to say why. That exit is real and
intermittent - it showed up on both a handset and an emulator - and
was undiagnosable in the field.

Keep at most 40 lines, and only from the core's first 10 seconds, then
log them when the exit code is non-zero. A core up longer than that may
be carrying traffic and its log can name destinations, which must never
reach the device log (rule 95), so past the window the buffer is
dropped rather than kept.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 15:37:29 +03:00
esefxdzandClaude Opus 5 4e5c7a02db fix(runtime): make Xray core recovery work, and stop it leaking a core per try
Core recovery could never succeed, and every attempt left a live
libxray.so behind.

startCore() calls startTimer(), which builds a CountDownTimer and so a
Handler on the calling thread. handleXrayCoreExit() runs recovery on a
bare Thread, so the constructor threw "Can't create handler inside
thread ... that has not called Looper.prepare()" and took the whole
restart down with it. The catch then deleted the ephemeral config and
returned false without killing the process pb.start() had already
started, so each retry stranded another core.

Measured on an emulator by killing the live core:

  before  1 -> 5 -> 6 -> 8 processes over 120 s, never recovers
  after   "Xray core recovered without dropping TUN" on attempt 1,
          exactly 1 process

startTimer now posts to the main looper, re-checking RUNTIME_TOKEN
inside the post so a stop that lands while it is queued cannot leave a
timer running for a dead generation. stopTimer posts too, so the
countDownTimer field is only touched from one thread. The catch clears
xrayProcess before destroying it, so the monitor thread's
`xrayProcess === process` guard fails and recovery is not re-entered
for a core being killed here.

Pre-existing on main; reachable whenever the core exits.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 15:37:29 +03:00
esefxdzandClaude Opus 5 30d96a4049 style: cut the comments down
Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 00:50:36 +03:00
esefxdzandClaude Opus 5 8ba3b6c543 fix(session): alert on every unsolicited native stop, not just the deadline
Second review pass found that VpnStatus.error is unreachable mid-session:
AppConfigs.V2RAY_STATES has no error state and the plugin's stateName when()
ends in else -> "DISCONNECTED", so Dart's unknown -> error mapping is dead
code on Android. Every native stop therefore arrived as a plain disconnect,
and anything other than the expiry deadline - an Xray or TUN setup failure, a
mid-session fail-closed deadline arm - left the user on their real IP with no
notification and no message. That is the exact hole this feature exists to
close.

stopAll() is the single funnel for every native stop, so the alert moves
there: it fires when the stop interrupted a live tunnel and the user did not
ask for it. isRunning is captured before cleanup() clears it, which keeps a
failed connect silent - it never had a tunnel to interrupt. STOP_SERVICE
(the notification's Disconnect action) and onRevoke pass userRequested: in
the revoke case the user switched VPNs or withdrew consent themselves, and
may well be protected by the app that took over.

Also from the pass:
- re-arming now needs the deadline to move by more than 15 s, so round-trip
  jitter cannot cancel and re-post a warning that is already on screen
- a warning delivered with under a minute left is dropped rather than
  announcing "5 minutes"; the disconnected alert follows anyway
- the _doDisconnect comment no longer claims endedMessage posts the alert
- CHANGELOG: the alert does now cover the tunnel stopping unexpectedly

flutter analyze clean; debug APK builds with the Kotlin recompiled.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-17 00:44:51 +03:00
esefxdzandClaude Opus 5 dd1082c8de fix(session): repair the expiry warning and the gaps the review found
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]>
2026-10-17 00:22:26 +03:00
esefxdzandClaude Opus 5 742f37683e feat(session): warn before a session ends and say when protection stops
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]>
2026-10-17 00:02:20 +03:00
esefxdz 6d8a923475 notification fixed 2026-09-06 13:07:37 +03:00
Naveax 5f851f3485 build: vendor pinned flutter_vless Android runtime (#10)
Replace build-time pub-cache mutation with a repository-owned pinned runtime so reviewed native source is the source compiled into the app.
2026-09-06 11:23:48 +03:00