diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java index b55a30621..c9c21c327 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java @@ -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) */ diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/WatchdogReceiver.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/WatchdogReceiver.java index 2aa11947f..067f41a29 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/WatchdogReceiver.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/WatchdogReceiver.java @@ -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++; diff --git a/src/frontends/android/app/src/test/java/org/strongswan/android/logic/WatchdogReceiverTest.kt b/src/frontends/android/app/src/test/java/org/strongswan/android/logic/WatchdogReceiverTest.kt index 09f0a1e23..bb0e4ae2b 100644 --- a/src/frontends/android/app/src/test/java/org/strongswan/android/logic/WatchdogReceiverTest.kt +++ b/src/frontends/android/app/src/test/java/org/strongswan/android/logic/WatchdogReceiverTest.kt @@ -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() } }