From 30d96a4049876d951e6e74f9e998961a07395f59 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Sat, 17 Oct 2026 00:50:36 +0300 Subject: [PATCH] style: cut the comments down Co-Authored-By: Claude Opus 5 --- .../kotlin/com/paladinvpn/app/MainActivity.kt | 5 +- lib/components/status_bar.dart | 11 +--- lib/logic/ad_manager.dart | 12 ++-- lib/logic/hivemind_service.dart | 13 ++-- lib/logic/notification_service.dart | 7 +-- lib/logic/session_timer.dart | 44 ++++--------- lib/logic/vpn_connection.dart | 5 +- .../settings/in_settings/connection_mode.dart | 6 +- .../tfox/flutter_vless/FlutterVlessPlugin.kt | 5 +- .../xray/service/XrayVPNService.kt | 61 +++++++------------ .../flutter_vless/xray/utils/RevoltAlerts.kt | 22 ++----- 11 files changed, 59 insertions(+), 132 deletions(-) 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 ac30ff4..662b60a 100644 --- a/android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt +++ b/android/app/src/main/kotlin/com/paladinvpn/app/MainActivity.kt @@ -70,10 +70,7 @@ class MainActivity : FlutterActivity() { updateVpnNotification(title, text, action) result.success(null) } - // Session ends only Dart can see (cap exhausted, server - // ended the session, VPN error). The VPN service posts the - // same alert for its own deadline, where this process may - // already be gone. RevoltAlerts owns both. + // Session ends only Dart sees; the service posts the rest. "showDisconnectedAlert" -> { RevoltAlerts.postDisconnected(this) result.success(null) diff --git a/lib/components/status_bar.dart b/lib/components/status_bar.dart index a539648..9727151 100644 --- a/lib/components/status_bar.dart +++ b/lib/components/status_bar.dart @@ -5,9 +5,7 @@ import 'package:revoltvpn/logic/app_colors.dart'; import 'package:revoltvpn/logic/session_timer.dart'; import 'package:revoltvpn/logic/vpn_connection.dart'; -// Nothing rendered the connection's own messages, so they are collected here -// in one place: whatever the user most needs to know right now, and whether it -// is a problem (red) or just progress (dim). +// The one line worth showing right now, and whether it is a problem. class _StatusDetail { const _StatusDetail(this.text, {this.isProblem = false}); @@ -26,8 +24,7 @@ _StatusDetail? _detail(VpnConnection vpn, SessionTimer timer) { return _StatusDetail(vpn.statusMessage); } - // The tunnel still carries traffic; only the control plane is missing, so - // this is a warning, not a disconnect (review #2). + // Control plane only - the tunnel still carries traffic. if (vpn.status == VpnStatus.connected && timer.serverUnreachable) { return const _StatusDetail( 'Can\'t reach the server — still connected', @@ -136,9 +133,7 @@ class StatusBar extends StatelessWidget { ], ), ), - // Until this existed, a failed connect, an expired session and a - // cap kill all just turned the button back to idle - the user - // could not tell protected from unprotected (review #3). + // Errors and session-end reasons used to render nowhere. if (detail != null) Padding( padding: const EdgeInsets.only(top: 6), diff --git a/lib/logic/ad_manager.dart b/lib/logic/ad_manager.dart index 7e0d225..307919a 100644 --- a/lib/logic/ad_manager.dart +++ b/lib/logic/ad_manager.dart @@ -96,18 +96,14 @@ class AdManager extends ChangeNotifier { // ── Show ad (or debug bypass) ───────────────────────────────────── - // Returns the nonce carried in the SSV custom_data when the reward was - // earned, null otherwise. The main flow passes it to vpn.connect(adNonce:) - // so the client waits for the session this ad created — minting a second - // nonce there is what broke every connect once ads were on. + // Returns the nonce put in the SSV custom_data, null if unrewarded. connect() + // must reuse it: minting a second nonce there rejects the ad's own session. Future showAd(String adType) async { if (!adsEnabled) { - // Main connect: no ad to show — HivemindService.fetchConfigDirectly() - // fires the server-gated bypass itself (rule 87). + // Main connect fires its own bypass in fetchConfigDirectly (rule 87). if (adType != 'support' || !kDebugMode) return null; - // Debug bypass: fire fake AdMob callback so support ads work in dev. - // The server's ADMOB_BYPASS must be True for this to succeed. + // Needs ADMOB_BYPASS=True server-side. final deviceId = await CryptoService.getDeviceId(); final nonce = HivemindService.newNonce(); try { diff --git a/lib/logic/hivemind_service.dart b/lib/logic/hivemind_service.dart index ac1daea..007bd71 100644 --- a/lib/logic/hivemind_service.dart +++ b/lib/logic/hivemind_service.dart @@ -81,15 +81,10 @@ class HivemindService { _expectedNonce = null; } - // Fetches this device's VLESS URL. Three ways in: - // adNonce != null — a real rewarded ad was watched; its SSV custom_data - // carried this nonce, so wait for a session holding it. - // Google's SSV callback can lag, so poll longer. - // resume == true — re-attach to the session already granted (mode-switch - // reconnect in connection_mode.dart). No ad, no bypass, - // any nonce accepted — device_id already owns it. - // neither — ads disabled: mint a nonce and fire the server-gated - // ADMOB_BYPASS callback (rule 87). + // Fetches this device's VLESS URL. + // adNonce — wait for the session that ad created (SSV can lag, so poll longer) + // resume — re-attach to the session already held; any nonce accepted + // neither — ads off: mint a nonce and fire the bypass (rule 87) static Future fetchConfigDirectly({ void Function(int attempt, int total)? onAttempt, String? adNonce, diff --git a/lib/logic/notification_service.dart b/lib/logic/notification_service.dart index c22a04a..bf830d2 100644 --- a/lib/logic/notification_service.dart +++ b/lib/logic/notification_service.dart @@ -32,15 +32,12 @@ class NotificationService { } } - // Posts the "disconnected - not protected" alert for a session end the user - // did not ask for. The native service posts the same alert (same id) when - // its own deadline fires, because by then this process may be gone; a double - // post replaces rather than stacks. + // Same id as the service's own alert, so a double post replaces. static Future showDisconnectedAlert() async { try { await _channel.invokeMethod('showDisconnectedAlert'); } catch (_) { - // Informational only - never worth failing a disconnect over. + // Informational only. } } diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index a7729a3..f5196da 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -42,10 +42,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { int _lastUsedBytes = 0; Stopwatch? _sinceLastSuccessfulSync; - // Wall-clock deadline, kept alongside the Stopwatch because they answer - // different questions. The Stopwatch is CLOCK_MONOTONIC: it does not advance - // while the device is suspended, which is exactly when an RTC_WAKEUP expiry - // alarm fires, so it cannot be used to recognise an expiry after sleep. + // Wall clock, because the Stopwatch is CLOCK_MONOTONIC and stops across + // device suspend - exactly when the RTC_WAKEUP expiry alarm fires. DateTime? _deadlineAt; double _currentSpeedKBps = 0.0; @@ -70,14 +68,10 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { int get usedBytes => _usedBytes; double get currentSpeedKBps => _currentSpeedKBps; - // True once the control plane has missed enough polls in a row to be worth - // telling the user about. It is a display state only: a tunnel that still - // carries traffic is never torn down because the API is unreachable (the - // native deadline and the server both still enforce expiry). + // Display state only - an unreachable API never tears down a live tunnel. bool get serverUnreachable => _consecutiveFailures >= _maxConsecutiveFailures; - // Why the last session ended, when the user did not end it themselves. - // Cleared on the next connect. Rendered by status_bar.dart. + // Why the last session ended, if the user did not end it. Cleared on connect. String? _lastEndMessage; String? get lastEndMessage => _lastEndMessage; @@ -146,19 +140,15 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { } if (vpnConnection.status == VpnStatus.disconnected) { - // 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. + // The native deadline beats the 1 s tick, so expiry lands here, not in + // _tick. postAlert is false: the service already posted, and a + // notification-Disconnect tap is the user asking. final deadline = _deadlineAt; final expired = _hasSyncedOnce && deadline != null && DateTime.now().isAfter( deadline.subtract(const Duration(seconds: 5)), ); - // 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: @@ -168,9 +158,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { } } - // 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. + // resetSupportClaim is false when re-attaching to a session already held: + // the top-up is once per server session and this flag is what enforces it. Future start({bool resetSupportClaim = true}) async { _sessionEpoch++; _remainingSeconds = 0; @@ -255,11 +244,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { await _doDisconnect(reason); } - // endedMessage is the one sentence the UI shows after an end the user did - // not ask for; postAlert independently decides whether to raise the "not - // protected" notification, which the native service already posts for every - // stop it sees. Both exist so nobody is left on their real IP without being - // told. A user-requested disconnect passes neither. + // endedMessage is what the UI shows; postAlert raises the notification. + // A user-requested disconnect passes neither. Future _doDisconnect( String reason, { String? endedMessage, @@ -428,12 +414,8 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { _remainingSeconds = (_remainingAtLastSync - elapsed).clamp(0, 1 << 31).toInt(); - // The Stopwatch is CLOCK_MONOTONIC and stops while the device is - // suspended, so after a long sleep it over-reports - and with the server - // unreachable no sync arrives to correct it, which is exactly the state - // the app now stays in rather than disconnecting. Take whichever is - // smaller, never the wall clock alone: a clock set backwards must not be - // able to extend a session. + // The Stopwatch over-reports after device suspend. Take the smaller of + // the two, never the wall clock alone - a backward clock must not extend. final deadline = _deadlineAt; if (deadline != null) { final byWallClock = deadline.difference(DateTime.now()).inSeconds; diff --git a/lib/logic/vpn_connection.dart b/lib/logic/vpn_connection.dart index 78150d2..788b8cf 100644 --- a/lib/logic/vpn_connection.dart +++ b/lib/logic/vpn_connection.dart @@ -224,9 +224,8 @@ 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. + // Proxy mode skips requestPermission below (VPN consent it never needs), + // so it has to ask for POST_NOTIFICATIONS on its own. if (!kIsWeb && _activeMode == ConnectionMode.proxy) { try { await _nativeControl.invokeMethod('requestNotificationPermission'); diff --git a/lib/screens/settings/in_settings/connection_mode.dart b/lib/screens/settings/in_settings/connection_mode.dart index 0d5e879..0f0fe32 100644 --- a/lib/screens/settings/in_settings/connection_mode.dart +++ b/lib/screens/settings/in_settings/connection_mode.dart @@ -67,10 +67,8 @@ class _ConnectionModeTileState extends State { widget.onChanged?.call(next); } - // Recovers from an orphaned proxy: tear the runtime down and re-attach to - // the session this device already holds (connect(resume: true)), so a - // readable set of credentials exists again. No rewarded ad and no new - // session — the user is repairing a connection they already paid for. + // Recovers an orphaned proxy: tear the runtime down and re-attach to the + // session already held. No ad, no new session. Future _reconnect() async { if (_reconnecting) return; final vpn = context.read(); diff --git a/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/FlutterVlessPlugin.kt b/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/FlutterVlessPlugin.kt index e71c891..57ea27c 100644 --- a/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/FlutterVlessPlugin.kt +++ b/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/FlutterVlessPlugin.kt @@ -240,10 +240,7 @@ class FlutterVlessPlugin : FlutterPlugin, ActivityAware, PluginRegistry.Activity } } - // POST_NOTIFICATIONS on its own, without VpnService.prepare: SOCKS5 - // mode never establishes a VPN interface, so it never called - // requestPermission and both session alerts were dropped unasked - // for on Android 13+. + // POST_NOTIFICATIONS without VpnService.prepare, for proxy mode. "requestNotificationPermission" -> { val currentActivity = activity if (currentActivity == null) { diff --git a/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/service/XrayVPNService.kt b/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/service/XrayVPNService.kt index d57796c..44675e1 100644 --- a/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/service/XrayVPNService.kt +++ b/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/service/XrayVPNService.kt @@ -67,9 +67,8 @@ class XrayVPNService : VpnService() { return START_NOT_STICKY } - // Must stay above the command handling below: an intent carrying no - // COMMAND falls through to the final else and stops a live tunnel - in - // this case five minutes early, which is what it warns about. + // Must stay above the command dispatch: an intent with no COMMAND + // falls through to the final else and stops the tunnel. if (intent.getBooleanExtra(EXTRA_SESSION_WARNING, false)) { handleSessionWarningAlarm(intent.getStringExtra("RUNTIME_TOKEN").orEmpty()) return START_NOT_STICKY @@ -428,12 +427,8 @@ class XrayVPNService : VpnService() { } } - // userRequested marks the two stops the user drove themselves. Everything - // else that reaches here while a tunnel is live - the expiry deadline, an - // Xray or TUN failure, a fail-closed deadline arm - took protection away - // without being asked, and must say so. isRunning is read before cleanup() - // clears it, which is also what keeps a failed connect silent: it never had - // a live tunnel to interrupt. + // Anything that stops a live tunnel unasked must say so. isRunning is read + // before cleanup() clears it, which keeps a failed connect silent. private fun stopAll( confirmationToken: String = currentConfig?.RUNTIME_TOKEN.orEmpty(), userRequested: Boolean = false, @@ -479,33 +474,26 @@ class XrayVPNService : VpnService() { else deadlineHandler.postDelayed(sessionDeadlineRunnable, delayMs) } - // Schedules the five-minute warning off the same deadline the expiry alarm - // holds. Unlike the deadline this is advisory: a warning that cannot be - // scheduled must never stop a working tunnel, so nothing here fails closed. - // Dart re-pushes the deadline on every sync, so this runs about once a minute. + // Advisory, unlike the deadline itself: nothing here fails closed. Dart + // re-pushes the deadline every sync, so this runs about once a minute. private fun armExpiryWarning(runtimeToken: String, expiresAtEpochMs: Long) { if (runtimeToken.isEmpty()) return val warnAt = expiresAtEpochMs - RevoltAlerts.WARNING_LEAD_MS - // Deadline already inside the warning window (or past): either the alarm - // has fired and its notification should stay up, or this is the short - // bootstrap deadline at connect, which must not warn at all. Nothing is - // cancelled here - Dart re-pushes the same deadline every 60 s with no - // dedupe, so cancelling on this branch would reliably delete the armed - // warning on the first sync after it came due and never re-arm it. + // Inside the window already (or the short bootstrap deadline). Cancel + // nothing here: with no dedupe on the re-push, cancelling on this + // branch deletes the armed warning and never re-arms it. if (warnAt <= System.currentTimeMillis() + WARNING_REARM_GRACE_MS) return cancelSessionWarningAlarm() - // More than the lead away, so any warning on screen belongs to an older - // deadline - a support top-up just pushed it out. + // A warning on screen now belongs to an older deadline (top-up). RevoltAlerts.cancelExpiryWarning(this) try { val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager val pendingIntent = sessionWarningPendingIntent(runtimeToken) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // Inexact on purpose: minute-level slop is fine for a warning, - // and it keeps this off the SCHEDULE_EXACT_ALARM path. + // Inexact on purpose - keeps this off SCHEDULE_EXACT_ALARM. alarmManager.setAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, warnAt, @@ -522,8 +510,7 @@ class XrayVPNService : VpnService() { private fun handleSessionWarningAlarm(alarmToken: String) { val config = currentConfig if (config == null) { - // The alarm woke a service process with no live runtime; there is - // no session left to warn about. + // Woke a process with no live runtime; nothing to warn about. stopSelf() return } @@ -537,9 +524,8 @@ class XrayVPNService : VpnService() { if (remaining < WARNING_FLOOR_MS || remaining > RevoltAlerts.WARNING_LEAD_MS + WARNING_SLACK_MS ) { - // Too late to be a five-minute warning, or for a deadline that has - // moved. Saying "5 minutes" with seconds left is worse than silence; - // the disconnected alert follows either way. + // Too late, or the deadline moved. Saying "5 minutes" with + // seconds left is worse than silence. Log.w(TAG, "Ignoring session warning outside the warning window") return } @@ -707,10 +693,8 @@ class XrayVPNService : VpnService() { private fun cancelSessionWarningAlarm() { try { val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager - // Look the existing PendingIntent up instead of building one: with - // FLAG_UPDATE_CURRENT, constructing it to cancel rewrites the live - // alarm's RUNTIME_TOKEN to "" first, and a delivery already in - // flight would then be dropped as a stale generation. + // Look it up rather than build it: FLAG_UPDATE_CURRENT would + // rewrite the live alarm's RUNTIME_TOKEN to "" before cancelling. existingSessionWarningPendingIntent()?.let { alarmManager.cancel(it) } } catch (e: Exception) { Log.w(TAG, "Failed to cancel session-warning alarm", e) @@ -725,9 +709,8 @@ class XrayVPNService : VpnService() { return PendingIntent.getService(this, SESSION_WARNING_REQUEST_CODE, intent, flags) } - // A PendingIntent is identified by component plus request code, not extras, - // so the warning needs its own code or it would overwrite - and later - // cancel - the expiry alarm. + // Identity is component + request code, not extras, so the warning needs + // its own code or it clobbers the expiry alarm. private fun sessionWarningPendingIntent(runtimeToken: String): PendingIntent { val intent = Intent(this, XrayVPNService::class.java) .putExtra(EXTRA_SESSION_WARNING, true) @@ -814,13 +797,11 @@ class XrayVPNService : VpnService() { private const val EXTRA_SESSION_WARNING = "SESSION_WARNING" private const val SESSION_EXPIRY_REQUEST_CODE = 1001 private const val SESSION_WARNING_REQUEST_CODE = 1002 - // An idle-capable alarm can run late; accept that much drift before - // treating a warning as belonging to a deadline that has moved. + // Drift allowed before a warning is treated as belonging elsewhere. private const val WARNING_SLACK_MS = 60_000L - // Don't re-arm (and so re-post) for a deadline that only moved by the - // jitter in a network round trip. + // Don't re-arm for a deadline that moved by round-trip jitter alone. private const val WARNING_REARM_GRACE_MS = 15_000L - // Below this the warning has lost its point; stay quiet instead. + // Below this the warning has lost its point. private const val WARNING_FLOOR_MS = 60_000L } } diff --git a/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/utils/RevoltAlerts.kt b/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/utils/RevoltAlerts.kt index 48ada2e..05c998a 100644 --- a/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/utils/RevoltAlerts.kt +++ b/local_packages/flutter_vless_android-1.1.5/android/src/main/kotlin/com/github/tfox/flutter_vless/xray/utils/RevoltAlerts.kt @@ -12,23 +12,16 @@ import android.util.Log // local_packages/.../xray/utils/RevoltAlerts.kt // -// The two user-facing session alerts, built in one place because two processes -// post them: XrayVPNService (:RunSoLibXrayDaemon) owns the native deadline and -// can still warn when the UI process is gone, while MainActivity posts the -// "not protected" alert for session ends only Dart can see (cap exhausted, -// server ended the session, VPN error). Both use the same ids, so whichever -// posts second just replaces the first notification instead of stacking. -// -// These are deliberately NOT the foreground-service notification (id 1, -// channel REVOLT_VPN_SERVICE, IMPORTANCE_LOW): an alert the user must notice -// needs its own high-importance channel, and must be dismissible. +// The two session alerts. Both XrayVPNService (:RunSoLibXrayDaemon) and +// MainActivity post them, sharing ids so a second post replaces. Separate from +// the foreground-service notification (id 1, IMPORTANCE_LOW): these must be +// noticed, and must be dismissible. object RevoltAlerts { const val CHANNEL_ID = "REVOLT_VPN_ALERTS" const val WARNING_NOTIFICATION_ID = 2 const val DISCONNECTED_NOTIFICATION_ID = 3 - // How long before the session deadline the warning fires. const val WARNING_LEAD_MS = 5 * 60 * 1000L private const val TAG = "RevoltAlerts" @@ -97,8 +90,7 @@ object RevoltAlerts { .setSmallIcon(icon) .setAutoCancel(true) .setOngoing(false) - // Both processes can post the disconnected alert; the second - // post replaces the first and must not sound again. + // A replacing post must not sound again. .setOnlyAlertOnce(true) launchIntent(context)?.let { builder.setContentIntent(it) } @@ -118,9 +110,7 @@ object RevoltAlerts { private fun manager(context: Context): NotificationManager? = context.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager - // Importance is fixed when the channel is first created, so this asks for - // HIGH up front — a session alert the user never sees is the whole bug - // these notifications exist to fix. + // Importance is fixed at creation, so ask for HIGH up front. private fun ensureChannel(manager: NotificationManager) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (manager.getNotificationChannel(CHANNEL_ID) != null) return