feat(session): warn before a session ends and say when protection stops

Review findings #1-#3 in internal/APP_REVIEW_2026-10-16.md: every session
end dropped the user onto their real IP with no warning, and the app showed
no errors at all.

- RevoltAlerts (vendored runtime) owns both alerts: a high-importance
  channel, ids 2 and 3, posted from whichever process is alive
- XrayVPNService schedules a 5-minute warning off the deadline it already
  holds, best effort (a warning that cannot be armed never stops a tunnel),
  with its own request code so it cannot clobber the expiry alarm
- the warning intent is handled above the command dispatch: without that it
  would fall through to the final else and stop the tunnel 5 minutes early
- SessionTimer._doDisconnect takes endedMessage: set for ends the user did
  not ask for, which both posts the alert and gives the UI a sentence
- the API being unreachable no longer disconnects a working tunnel; it is a
  display state now, with expiry still enforced natively and server-side
- status_bar.dart renders connect errors, connecting progress and the
  session-end reason

flutter analyze clean; debug APK builds (Kotlin compiles).

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
esefxdzandClaude Opus 5 committed 2026-10-17 00:02:20 +03:00
1 parent 37753c6396
commit 742f37683e
7 files changed
+401 -26

No files matched your search

@@ -20,6 +20,7 @@ import android.os.VibratorManager
import android.provider.Settings
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.github.tfox.flutter_vless.xray.utils.RevoltAlerts
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
@@ -61,14 +62,23 @@ class MainActivity : FlutterActivity() {
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, notificationChannel)
.setMethodCallHandler { call, result ->
if (call.method == "updateNotificationText") {
val text = call.argument<String>("text") ?: ""
val title = call.argument<String>("title") ?: "Revolt VPN"
val action = call.argument<String>("disconnectLabel") ?: "Disconnect"
updateVpnNotification(title, text, action)
result.success(null)
} else {
result.notImplemented()
when (call.method) {
"updateNotificationText" -> {
val text = call.argument<String>("text") ?: ""
val title = call.argument<String>("title") ?: "Revolt VPN"
val action = call.argument<String>("disconnectLabel") ?: "Disconnect"
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.
"showDisconnectedAlert" -> {
RevoltAlerts.postDisconnected(this)
result.success(null)
}
else -> result.notImplemented()
}
}