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]>
This commit is contained in:
1 parent
742f37683e
commit
dd1082c8de
7 files changed
+96
-18
No files matched your search
@@ -141,14 +141,28 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
if (vpnConnection.status == VpnStatus.disconnected) {
|
||||
// No alert here. The runtime going away covers both the native deadline
|
||||
// (which posts the alert itself, from a process that outlives this one)
|
||||
// and the notification's Disconnect action, which the user tapped.
|
||||
unawaited(_doDisconnect('VPN tunnel dropped'));
|
||||
// The native deadline fires on the exact second while this 1 s tick is
|
||||
// in arbitrary phase, so expiry almost always arrives here rather than
|
||||
// at the countdown check in _tick - without this, the commonest session
|
||||
// end left the UI with nothing to say.
|
||||
_reconcileElapsedTime();
|
||||
final expired = _hasSyncedOnce && _remainingSeconds <= 2;
|
||||
// postAlert is false either way: the native side already posted for its
|
||||
// own deadline, and the notification's Disconnect action is the user
|
||||
// asking, which must never raise a "not protected" alert.
|
||||
unawaited(_doDisconnect(
|
||||
'VPN tunnel dropped',
|
||||
endedMessage:
|
||||
expired ? 'Your session ended. You are not protected.' : null,
|
||||
postAlert: false,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> start() async {
|
||||
// resetSupportClaim is false when re-attaching to a session the device
|
||||
// already holds: the support top-up is once per server session, and the
|
||||
// claim flag is the only thing enforcing that.
|
||||
Future<void> start({bool resetSupportClaim = true}) async {
|
||||
_sessionEpoch++;
|
||||
_remainingSeconds = 0;
|
||||
_remainingAtLastSync = 0;
|
||||
@@ -165,10 +179,12 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
_lastEndMessage = null;
|
||||
NotificationService.reset();
|
||||
|
||||
_supportStateEpoch++;
|
||||
_supportRewardClaimed = false;
|
||||
_supportRewardStateLoaded = true;
|
||||
unawaited(_persistSupportRewardState(false));
|
||||
if (resetSupportClaim) {
|
||||
_supportStateEpoch++;
|
||||
_supportRewardClaimed = false;
|
||||
_supportRewardStateLoaded = true;
|
||||
unawaited(_persistSupportRewardState(false));
|
||||
}
|
||||
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), _tick);
|
||||
@@ -233,7 +249,11 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
// not ask for, and its presence is also what posts the "not protected"
|
||||
// notification. Both exist so nobody is left on their real IP without being
|
||||
// told. A user-requested disconnect passes nothing.
|
||||
Future<void> _doDisconnect(String reason, {String? endedMessage}) async {
|
||||
Future<void> _doDisconnect(
|
||||
String reason, {
|
||||
String? endedMessage,
|
||||
bool postAlert = true,
|
||||
}) async {
|
||||
if (_isDisconnecting) return;
|
||||
_isDisconnecting = true;
|
||||
_sessionEpoch++;
|
||||
@@ -252,7 +272,7 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver {
|
||||
notifyListeners();
|
||||
|
||||
await vpnConnection.disconnect();
|
||||
if (endedMessage != null) {
|
||||
if (endedMessage != null && postAlert) {
|
||||
unawaited(NotificationService.showDisconnectedAlert());
|
||||
}
|
||||
|
||||
|
||||
@@ -224,6 +224,18 @@ class VpnConnection extends ChangeNotifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SOCKS5 mode skips requestPermission below (it would raise the VPN
|
||||
// consent dialog for an interface it never creates), so ask for the
|
||||
// notification permission on its own - the session alerts need it.
|
||||
if (!kIsWeb && _activeMode == ConnectionMode.proxy) {
|
||||
try {
|
||||
await _nativeControl.invokeMethod<void>('requestNotificationPermission');
|
||||
} catch (e) {
|
||||
debugPrint('[VPN] Notification permission request failed: $e');
|
||||
}
|
||||
if (!_isCurrentConnect(connectEpoch)) return false;
|
||||
}
|
||||
|
||||
if (!kIsWeb && _activeMode == ConnectionMode.tun) {
|
||||
final ok = await _vless.requestPermission();
|
||||
if (!_isCurrentConnect(connectEpoch)) return false;
|
||||
|
||||
Reference in new issue
Block a user