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 <[email protected]>
This commit is contained in:
1 parent
e7ef251882
commit
eab43b0420
1 file changed
+20
+20
@@ -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
|
||||
|
||||
Reference in new issue
Block a user