From eab43b04207a2f88106389c01c988f036a0cc8e9 Mon Sep 17 00:00:00 2001 From: esefxdz Date: Sat, 17 Oct 2026 16:14:48 +0300 Subject: [PATCH] fix(session): deliver the expiry warning when the app is closed The five-minute warning did not arrive in the one case it exists for. The deadline is enforced by two things - a main-looper Handler and an alarm - while the warning had only the alarm, and that alarm is inexact on purpose so the app can stay off SCHEDULE_EXACT_ALARM. Doze defers an inexact alarm freely, and with the app closed it deferred this one past the end of the session it was warning about. Measured on an emulator, app swiped from recents and its process killed, screen off, deviceidle forced to IDLE, 7-minute session: before warning never fired; request code 1002 never delivered and handleSessionWarningAlarm never ran. Expiry was still punctual (16:00:55 against a 16:00:54 deadline) because the deadline has its Handler. after warning posted as the session crossed 300 s remaining, app still closed and device still IDLE; expiry unchanged. So give the warning the same backstop that already makes the deadline reliable. postDelayed runs on uptimeMillis, so a genuinely suspended device still fires it late - handleSessionWarningAlarm then drops it on the existing window check instead of claiming "5 minutes" with seconds left. The alarm stays for the case where the process was restarted and no Handler callback survives. Note this does not make the warning punctual under true suspend; only SCHEDULE_EXACT_ALARM would, and that permission is deliberately not declared. Co-Authored-By: Claude Opus 5 --- .../xray/service/XrayVPNService.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) 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 44675e1..67a6e4e 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 @@ -45,6 +45,14 @@ class XrayVPNService : VpnService() { private var sessionDeadlineToken: String? = null private val sessionDeadlineRunnable = Runnable { enforceSessionDeadline() } + // The warning's own backstop, mirroring the deadline's. The alarm alone is + // not enough: it is inexact on purpose (no SCHEDULE_EXACT_ALARM), and with + // the app closed on a dozing device it was measured never arriving at all, + // while the deadline's Handler fired within a second of its target. + private val sessionWarningRunnable = Runnable { + handleSessionWarningAlarm(sessionDeadlineToken.orEmpty()) + } + override fun onCreate() { super.onCreate() } @@ -505,6 +513,17 @@ class XrayVPNService : VpnService() { } catch (e: Exception) { Log.w(TAG, "Failed to schedule session-expiry warning", e) } + + // postDelayed runs on uptimeMillis, so a truly suspended device still + // fires this late - handleSessionWarningAlarm then rejects it on the + // window check rather than claiming "5 minutes" with seconds left. + // Whenever the process is merely backgrounded, which is the common + // case, this is what actually delivers the warning. + val warnDelay = warnAt - System.currentTimeMillis() + deadlineHandler.removeCallbacks(sessionWarningRunnable) + if (warnDelay > 0L) { + deadlineHandler.postDelayed(sessionWarningRunnable, warnDelay) + } } private fun handleSessionWarningAlarm(alarmToken: String) { @@ -691,6 +710,7 @@ class XrayVPNService : VpnService() { } private fun cancelSessionWarningAlarm() { + deadlineHandler.removeCallbacks(sessionWarningRunnable) try { val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager // Look it up rather than build it: FLAG_UPDATE_CURRENT would