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 6280fdf5c..78706d228 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 @@ -41,6 +41,7 @@ import org.strongswan.android.ui.VpnProfileControlActivity; import java.lang.ref.WeakReference; import java.util.Collections; +import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -199,6 +200,27 @@ public class VpnStateService extends Service mListeners.remove(listener); } + /** + * Notify all registered listeners about a state change. Iterates over a + * copy (a listener may unregister itself from its callback) and isolates + * the listeners from each other: one failing listener must neither skip + * the others nor crash the main thread. + */ + private void notifyStateListeners() + { + for (VpnStateListener listener : new ArrayList<>(mListeners)) + { + try + { + listener.stateChanged(); + } + catch (RuntimeException e) + { + DiagLog.i(TAG, "state listener failed: " + e); + } + } + } + /** * Get the current VPN profile. * @@ -373,7 +395,15 @@ public class VpnStateService extends Service Context context = getApplicationContext(); Intent intent = new Intent(context, CharonVpnService.class); intent.setAction(CharonVpnService.DISCONNECT_ACTION); - context.startService(intent); + try + { + context.startService(intent); + } + catch (IllegalStateException e) + { /* background start of a service that is not running (Android 8+): + * without the service there is no tunnel to tear down */ + DiagLog.i(TAG, "disconnect: VPN service not running (" + e.getMessage() + ")"); + } } /** @@ -589,10 +619,7 @@ public class VpnStateService extends Service { if (change.call()) { /* otherwise there is no need to notify the listeners */ - for (VpnStateListener listener : mListeners) - { - listener.stateChanged(); - } + notifyStateListeners(); /* the home screen widget mirrors the state */ VpnProfile widgetProfile = mProfile; ShxWidgetState.publish(VpnStateService.this, mState, getErrorState(), @@ -602,7 +629,7 @@ public class VpnStateService extends Service } catch (Exception e) { - e.printStackTrace(); + DiagLog.i(TAG, "state change failed: " + e); } } }); @@ -766,6 +793,9 @@ public class VpnStateService extends Service mRetryTimeout = mRetryIn = mTimeoutProvider.getTimeout(error); mRetryFrozen = false; 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) */ + mHandler.removeMessages(RETRY_MSG); if (mRetryTimeout <= 0) { return; @@ -778,6 +808,7 @@ public class VpnStateService extends Service */ private void resetRetryTimer() { + mHandler.removeMessages(RETRY_MSG); mRetryTimeout = 0; mRetryIn = 0; mRetryFrozen = false; @@ -858,10 +889,7 @@ public class VpnStateService extends Service /* calculate next interval before notifying listeners */ long next = SystemClock.uptimeMillis() + retryStep(service.mRetryIn); - for (VpnStateListener listener : service.mListeners) - { - listener.stateChanged(); - } + service.notifyStateListeners(); sendMessageAtTime(obtainMessage(RETRY_MSG), next); } else @@ -872,10 +900,7 @@ public class VpnStateService extends Service * ticking every second */ service.mRetryFrozen = true; service.mRetryIn = RETRY_SUPPRESS_MS; - for (VpnStateListener listener : service.mListeners) - { - listener.stateChanged(); - } + service.notifyStateListeners(); sendMessageAtTime(obtainMessage(RETRY_MSG), SystemClock.uptimeMillis() + RETRY_SUPPRESS_MS); return; }