diff --git a/CHANGELOG.md b/CHANGELOG.md index ece4ba2..dcfdacd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,8 @@ Versions follow [semver](https://semver.org/): MAJOR.MINOR.PATCH off the same deadline it already enforces, so the warning still arrives when the app is closed, and a support top-up moves it with the deadline. - **"Disconnected — not protected" notification** when a session ends without - you asking: the 2-hour expiry, the data limit, or the server ending the - session. + you asking: the 2-hour expiry, the data limit, the server ending the + session, or the tunnel stopping unexpectedly. - **SOCKS5 mode now asks for the notification permission.** It never did, because it never raises the VPN consent dialog, so on Android 13+ these alerts would have been dropped before they were shown. diff --git a/lib/logic/session_timer.dart b/lib/logic/session_timer.dart index 9e8b138..a7729a3 100644 --- a/lib/logic/session_timer.dart +++ b/lib/logic/session_timer.dart @@ -256,9 +256,10 @@ class SessionTimer extends ChangeNotifier with WidgetsBindingObserver { } // endedMessage is the one sentence the UI shows after an end the user did - // 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. + // 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. Future _doDisconnect( String reason, { String? endedMessage, 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 9ef07e0..d57796c 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 @@ -91,7 +91,7 @@ class XrayVPNService : VpnService() { Log.w(TAG, "Ignoring STOP_SERVICE without matching runtime generation") return START_NOT_STICKY } - stopAll(requestedToken) + stopAll(requestedToken, userRequested = true) return START_NOT_STICKY } @@ -428,14 +428,25 @@ class XrayVPNService : VpnService() { } } - private fun stopAll(confirmationToken: String = currentConfig?.RUNTIME_TOKEN.orEmpty()) { + // 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. + private fun stopAll( + confirmationToken: String = currentConfig?.RUNTIME_TOKEN.orEmpty(), + userRequested: Boolean = false, + ) { if (shuttingDownIntentionally) return + val alertUser = isRunning && !userRequested shuttingDownIntentionally = true cleanup(clearDeadline = true) currentConfig = null XrayCoreManager.stopCore(this, confirmationToken) stopForeground(true) stopSelf() + if (alertUser) RevoltAlerts.postDisconnected(this) } private fun armSessionDeadline(config: XrayConfig, remainingSeconds: Long) { @@ -482,7 +493,7 @@ class XrayVPNService : VpnService() { // 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. - if (warnAt <= System.currentTimeMillis()) return + if (warnAt <= System.currentTimeMillis() + WARNING_REARM_GRACE_MS) return cancelSessionWarningAlarm() // More than the lead away, so any warning on screen belongs to an older @@ -523,7 +534,12 @@ class XrayVPNService : VpnService() { val deadline = sessionDeadlineElapsed ?: return val remaining = deadline - SystemClock.elapsedRealtime() - if (remaining <= 0L || remaining > RevoltAlerts.WARNING_LEAD_MS + WARNING_SLACK_MS) { + 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. Log.w(TAG, "Ignoring session warning outside the warning window") return } @@ -553,10 +569,6 @@ class XrayVPNService : VpnService() { } Log.w(TAG, "Native session deadline expired; stopping runtime fail-closed") stopAll(token) - // Only the deadline paths post this natively. A STOP_SERVICE command - // cannot tell a user tap from a Dart-side session end, so Dart posts for - // its own reasons; the shared id means a double post just replaces. - RevoltAlerts.postDisconnected(this) } private fun updateSessionDeadline(intent: Intent) { @@ -646,8 +658,9 @@ class XrayVPNService : VpnService() { RevoltAlerts.cancelExpiryWarning(this) stopForeground(true) stopSelf() + // This arm does not go through stopAll, so it posts for itself. + RevoltAlerts.postDisconnected(this) } - RevoltAlerts.postDisconnected(this) } private fun scheduleSessionExpiryAlarm(runtimeToken: String, expiresAtEpochMs: Long): Boolean { @@ -745,7 +758,7 @@ class XrayVPNService : VpnService() { } override fun onRevoke() { - stopAll() + stopAll(userRequested = true) super.onRevoke() } @@ -804,5 +817,10 @@ class XrayVPNService : VpnService() { // An idle-capable alarm can run late; accept that much drift before // treating a warning as belonging to a deadline that has moved. 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. + private const val WARNING_REARM_GRACE_MS = 15_000L + // Below this the warning has lost its point; stay quiet instead. + private const val WARNING_FLOOR_MS = 60_000L } } diff --git a/pubspec.lock b/pubspec.lock index 2af4fa8..b734022 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -299,10 +299,10 @@ packages: dependency: transitive description: name: matcher - sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.20" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -315,10 +315,10 @@ packages: dependency: transitive description: name: meta - sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" nested: dependency: transitive description: @@ -560,10 +560,10 @@ packages: dependency: transitive description: name: test_api - sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.11" typed_data: dependency: transitive description: @@ -648,10 +648,10 @@ packages: dependency: transitive description: name: vector_math - sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47 + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.2.0" vm_service: dependency: transitive description: @@ -725,5 +725,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.11.0-0 <4.0.0" + dart: ">=3.10.3 <4.0.0" flutter: ">=3.38.4"