android: watchdog backs off whenever a live process owns recovery

Retrying, connecting or whitelist-blocked sessions kept the 60 s alarm and
the 10 min Doze wakeup for hours although a live process makes the alarm
useless. They now share the connected backoff (5 min / 60 min).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DenozordecandClaude Opus 5.5 committed 2026-10-10 09:12:22 +07:00
1 parent 24e536b9b9
commit cf89cec20e
2 files changed
+46 -12

No files matched your search

@@ -150,6 +150,19 @@ public class WatchdogReceiver extends BroadcastReceiver
return Action.RESTART; return Action.RESTART;
} }
/**
* 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.
*/
static boolean isAliveSteady(Action action, boolean processAlive, boolean parked)
{
return action == Action.NONE && processAlive && !parked;
}
@Override @Override
public void onReceive(Context context, Intent intent) public void onReceive(Context context, Intent intent)
{ {
@@ -164,27 +177,24 @@ public class WatchdogReceiver extends BroadcastReceiver
return; return;
} }
reschedule(context); reschedule(context);
if (WhiteListManager.getInstance(context).shouldBlockReconnect()) /* the ISP whitelist blocks the server; the in-process retry
{ /* the ISP whitelist blocks the server; the in-process retry * machinery parks itself in that case, so should we */
* machinery parks itself in that case, so should we */ boolean whitelistBlocks = WhiteListManager.getInstance(context).shouldBlockReconnect();
DiagLog.i(TAG, "whitelist blocks the server, not restarting");
return;
}
VpnStateService service = VpnStateService.getLiveInstance(); VpnStateService service = VpnStateService.getLiveInstance();
boolean processAlive = service != null; boolean processAlive = service != null;
boolean serviceTracking = processAlive && service.getState() != VpnStateService.State.DISABLED; boolean serviceTracking = processAlive && service.getState() != VpnStateService.State.DISABLED;
boolean wifiPresent = isWifiPresent(context); boolean wifiPresent = isWifiPresent(context);
boolean autoProfileConfigured = AutoConnectManager.getInstance(context).getCurrentConfig().getProfileUuid() != null; boolean autoProfileConfigured = AutoConnectManager.getInstance(context).getCurrentConfig().getProfileUuid() != null;
Action action = decideAction(enabled, uuid != null, autoExpected, false, serviceTracking, Action action = decideAction(enabled, uuid != null, autoExpected, whitelistBlocks, serviceTracking,
processAlive, wifiPresent, autoProfileConfigured); processAlive, wifiPresent, autoProfileConfigured);
boolean connected = serviceTracking && service.getState() == VpnStateService.State.CONNECTED;
boolean parked = uuid == null && autoExpected && processAlive && !serviceTracking && action == Action.NONE; boolean parked = uuid == null && autoExpected && processAlive && !serviceTracking && action == Action.NONE;
boolean alive = isAliveSteady(action, processAlive, parked);
if (parked) if (parked)
{ {
sParkedChecks++; sParkedChecks++;
sConnectedChecks = 0; sConnectedChecks = 0;
} }
else if (connected && action == Action.NONE) else if (alive)
{ {
sConnectedChecks++; sConnectedChecks++;
sParkedChecks = 0; sParkedChecks = 0;
@@ -195,8 +205,8 @@ public class WatchdogReceiver extends BroadcastReceiver
sConnectedChecks = 0; sConnectedChecks = 0;
} }
boolean parkedSteady = parked && sParkedChecks >= PARKED_BACKOFF_AFTER; boolean parkedSteady = parked && sParkedChecks >= PARKED_BACKOFF_AFTER;
boolean connectedSteady = connected && sConnectedChecks >= CONNECTED_BACKOFF_AFTER; boolean connectedSteady = alive && sConnectedChecks >= CONNECTED_BACKOFF_AFTER;
String cadence = parkedSteady ? "parked" : connectedSteady ? "connected" : null; String cadence = parkedSteady ? "parked" : connectedSteady ? "alive" : null;
if (cadence != null) if (cadence != null)
{ /* steady state: log the transition once, then stay silent - a { /* steady state: log the transition once, then stay silent - a
* per-fire line only floods diag.log and burns flash I/O */ * per-fire line only floods diag.log and burns flash I/O */
@@ -210,7 +220,7 @@ public class WatchdogReceiver extends BroadcastReceiver
{ {
DiagLog.i(TAG, "check: uuid=" + (uuid != null) + " autoExpected=" + autoExpected + DiagLog.i(TAG, "check: uuid=" + (uuid != null) + " autoExpected=" + autoExpected +
" processAlive=" + processAlive + " tracking=" + serviceTracking + " processAlive=" + processAlive + " tracking=" + serviceTracking +
" wifi=" + wifiPresent + " -> " + action); " whitelist=" + whitelistBlocks + " wifi=" + wifiPresent + " -> " + action);
} }
sLastCadence = cadence; sLastCadence = cadence;
switch (action) switch (action)
@@ -125,4 +125,28 @@ class WatchdogReceiverTest
assertThat(decide(expectedProfileSet = true, autoExpected = true, processAlive = false, wifiPresent = false)) assertThat(decide(expectedProfileSet = true, autoExpected = true, processAlive = false, wifiPresent = false))
.isEqualTo(Action.RESTART) .isEqualTo(Action.RESTART)
} }
@Test
fun liveProcessRetryingOrWhitelistedBacksOff()
{
/* 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()
/* 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()
}
@Test
fun deadProcessOrRestartNeverBacksOff()
{
val restart = decide(expectedProfileSet = true, processAlive = false)
assertThat(WatchdogReceiver.isAliveSteady(restart, 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()
/* parked sessions use their own backoff */
assertThat(WatchdogReceiver.isAliveSteady(Action.NONE, true, true)).isFalse()
}
} }