android: a retry the device slept through is resumed by the watchdog alarm: the countdown runs in uptime and stood still in deep sleep, so a tunnel that dropped overnight came back only when the phone was picked up; while a retry is pending the wakeup alarm stays at 10 min instead of backing off to 1 h

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Fable 5.2 committed 2026-11-10 09:45:56 +07:00
1 parent b9ea35832c
commit f075bf00fe
3 files changed
+58 -14

No files matched your search

@@ -90,6 +90,9 @@ public class VpnStateService extends Service
private long mRetryIn;
/* true while the countdown is parked because the ISP whitelist blocks the server */
private boolean mRetryFrozen;
/* when the pending retry is due in elapsedRealtime: the countdown itself
* runs in uptime and stands still while the device sleeps */
private long mRetryDue;
/* timestamp of the last network-change-triggered immediate retry */
private long mLastNetworkRetry;
/* double-start guard: profile and issue time of the last connect() that
@@ -610,6 +613,37 @@ public class VpnStateService extends Service
});
}
/** Whether a retry countdown is pending (an error with an automatic retry). */
public boolean isRetryPending()
{
return mRetryTimeout > 0;
}
/**
* Runs the pending retry now if its time has passed on the wall clock.
* The countdown ticks in uptime, so it stands still while the device
* sleeps; the watchdog's wakeup alarm calls this so a tunnel that dropped
* overnight comes back without the user picking up the phone. Never
* shortens a backoff. Main thread only.
*
* @return true if a reconnect was started
*/
public boolean retryIfOverdue()
{
if (mRetryTimeout <= 0 || mProfile == null || SystemClock.elapsedRealtime() < mRetryDue ||
WhiteListManager.getInstance(this).shouldBlockReconnect())
{
return false;
}
DiagLog.i(TAG, "retry overdue (device slept through the countdown), reconnecting");
mRetryTimeout = 0;
mRetryIn = 0;
mRetryFrozen = false;
mHandler.removeMessages(RETRY_MSG);
connect(null, false);
return true;
}
/**
* Update state and notify all listeners about the change. By using a Handler
* this is done from the main UI thread and not the initial reporter thread.
@@ -826,6 +860,7 @@ public class VpnStateService extends Service
{
mRetryTimeout = mRetryIn = mTimeoutProvider.getTimeout(error);
mRetryFrozen = false;
mRetryDue = SystemClock.elapsedRealtime() + mRetryTimeout;
DiagLog.i(TAG, "error " + error + ", retry in " + (mRetryTimeout / 1000) + "s");
/* never run two countdown chains in parallel (double-speed countdown,
* double notification updates and two reconnects at zero) */
@@ -154,15 +154,16 @@ public class WatchdogReceiver extends BroadcastReceiver
/**
* Whether a check counts towards the steady-state backoff of a live
* process: connected, retrying/connecting, or held back by the ISP
* whitelist. In all of these the in-process machinery owns recovery and
* the alarm can only guard against the process dying - firing every
* minute (and waking Doze every 10 minutes) for hours of failed retries
* or whitelist blocking only burns battery.
* process: connected, connecting, or held back by the ISP whitelist. In
* all of these the in-process machinery owns recovery and the alarm can
* only guard against the process dying - firing every minute (and waking
* Doze every 10 minutes) for hours of whitelist blocking only burns
* battery.
*/
static boolean isAliveSteady(Action action, boolean processAlive, boolean parked)
{
return action == Action.NONE && processAlive && !parked;
static boolean isAliveSteady(Action action, boolean processAlive, boolean parked, boolean retryPending)
{ /* a pending retry needs the fast wakeup alarm: its countdown stands
* still while the device sleeps and only this alarm resumes it */
return action == Action.NONE && processAlive && !parked && !retryPending;
}
@Override
@@ -190,7 +191,13 @@ public class WatchdogReceiver extends BroadcastReceiver
Action action = decideAction(enabled, uuid != null, autoExpected, whitelistBlocks, serviceTracking,
processAlive, wifiPresent, autoProfileConfigured);
boolean parked = uuid == null && autoExpected && processAlive && !serviceTracking && action == Action.NONE;
boolean alive = isAliveSteady(action, processAlive, parked);
/* the whitelist parks retries on purpose, that state may back off */
boolean retryPending = serviceTracking && !whitelistBlocks && service.isRetryPending();
if (retryPending && service.retryIfOverdue())
{
DiagLog.i(TAG, "resumed a retry the device slept through");
}
boolean alive = isAliveSteady(action, processAlive, parked, retryPending);
if (parked)
{
sParkedChecks++;
@@ -132,21 +132,23 @@ class WatchdogReceiverTest
/* retrying/connecting: the service tracks the connection */
val retrying = decide(expectedProfileSet = true, serviceTracking = true, processAlive = true)
assertThat(retrying).isEqualTo(Action.NONE)
assertThat(WatchdogReceiver.isAliveSteady(retrying, true, false)).isTrue()
assertThat(WatchdogReceiver.isAliveSteady(retrying, true, false, false)).isTrue()
/* a pending retry stands still while the device sleeps: keep the fast wakeup alarm */
assertThat(WatchdogReceiver.isAliveSteady(retrying, true, false, true)).isFalse()
/* whitelist blocks the server while the process is alive */
val blocked = decide(expectedProfileSet = true, whitelistBlocks = true, processAlive = true)
assertThat(WatchdogReceiver.isAliveSteady(blocked, true, false)).isTrue()
assertThat(WatchdogReceiver.isAliveSteady(blocked, true, false, false)).isTrue()
}
@Test
fun deadProcessOrRestartNeverBacksOff()
{
val restart = decide(expectedProfileSet = true, processAlive = false)
assertThat(WatchdogReceiver.isAliveSteady(restart, false, false)).isFalse()
assertThat(WatchdogReceiver.isAliveSteady(restart, false, false, false)).isFalse()
/* whitelist blocks, but no live process: keep the fast cadence */
val blocked = decide(expectedProfileSet = true, whitelistBlocks = true, processAlive = false)
assertThat(WatchdogReceiver.isAliveSteady(blocked, false, false)).isFalse()
assertThat(WatchdogReceiver.isAliveSteady(blocked, false, false, false)).isFalse()
/* parked sessions use their own backoff */
assertThat(WatchdogReceiver.isAliveSteady(Action.NONE, true, true)).isFalse()
assertThat(WatchdogReceiver.isAliveSteady(Action.NONE, true, true, false)).isFalse()
}
}