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]>
63 lines
2.0 KiB
Dart
63 lines
2.0 KiB
Dart
import 'package:flutter/services.dart';
|
|
|
|
// ── lib/logic/notification_service.dart ─────────────────────────────────────
|
|
// Bridge to the native VPN notification, which flutter_vless owns. This only
|
|
// refreshes its content text with the session countdown, so a failure here is
|
|
// never worth propagating to the session timer.
|
|
|
|
class NotificationService {
|
|
NotificationService._();
|
|
|
|
static const MethodChannel _channel =
|
|
MethodChannel('com.revoltvpn.app/notification');
|
|
|
|
static String? _lastText;
|
|
|
|
/// Sets the notification's content text to [text].
|
|
///
|
|
/// The native side rebuilds the whole notification, so skip the round-trip
|
|
/// when the text has not changed. A failed native update is intentionally
|
|
/// not cached so the next tick can retry it.
|
|
static Future<void> updateTimer(String text) async {
|
|
if (text == _lastText) return;
|
|
try {
|
|
await _channel.invokeMethod<void>('updateNotificationText', {
|
|
'title': 'Revolt VPN',
|
|
'text': text,
|
|
'disconnectLabel': 'Disconnect',
|
|
});
|
|
_lastText = text;
|
|
} catch (_) {
|
|
// Cosmetic.
|
|
}
|
|
}
|
|
|
|
// Same id as the service's own alert, so a double post replaces.
|
|
static Future<void> showDisconnectedAlert() async {
|
|
try {
|
|
await _channel.invokeMethod<void>('showDisconnectedAlert');
|
|
} catch (_) {
|
|
// Informational only.
|
|
}
|
|
}
|
|
|
|
/// Takes down the countdown notification.
|
|
///
|
|
/// [updateTimer] re-posts an ongoing, non-dismissible notification, so a tick
|
|
/// that lands after the VPN service has cancelled its own foreground copy
|
|
/// leaves a frozen countdown on screen that nobody owns. Call this once the
|
|
/// tunnel is actually down.
|
|
static Future<void> cancelTimer() async {
|
|
_lastText = null;
|
|
try {
|
|
await _channel.invokeMethod<void>('cancelNotification');
|
|
} catch (_) {
|
|
// Cosmetic.
|
|
}
|
|
}
|
|
|
|
/// Forces the next connect to re-post even if the countdown resumes on the
|
|
/// same value.
|
|
static void reset() => _lastText = null;
|
|
}
|