diff --git a/android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt b/android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt index 662b60a..d3cbfb3 100644 --- a/android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt @@ -75,6 +75,18 @@ class MainActivity : FlutterActivity() { RevoltAlerts.postDisconnected(this) result.success(null) } + // The countdown re-post is ongoing and non-dismissible, so + // whoever posted it last has to take it down. A tick that + // lands after the service cancelled its own foreground + // notification otherwise strands a frozen countdown that + // claims a protected session forever. Harmless while the + // service is still in the foreground - the platform keeps + // its notification regardless. + "cancelNotification" -> { + (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager) + .cancel(NOTIFICATION_ID) + result.success(null) + } else -> result.notImplemented() } } diff --git a/lib/logic/notification_service.dart b/lib/logic/notification_service.dart index bf830d2..e3ee070 100644 --- a/lib/logic/notification_service.dart +++ b/lib/logic/notification_service.dart @@ -41,6 +41,21 @@ class NotificationService { } } + /// 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 cancelTimer() async { + _lastText = null; + try { + await _channel.invokeMethod('cancelNotification'); + } catch (_) { + // Cosmetic. + } + } + /// Forces the next connect to re-post even if the countdown resumes on the /// same value. static void reset() => _lastText = null; diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index f5196da..d55ee1c 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -270,6 +270,10 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { notifyListeners(); await vpnConnection.disconnect(); + // After the service is down, so the cancel is not swallowed by a still + // foreground notification. Without it a revoke leaves the countdown on + // screen, frozen, claiming a session that no longer protects anything. + await NotificationService.cancelTimer(); if (endedMessage != null && postAlert) { unawaited(NotificationService.showDisconnectedAlert()); }