Commit d591d5538b for strongswan.org

commit d591d5538bbda7d8a046f94176a144665b169f34
Author: Tobias Brunner <tobias@strongswan.org>
Date:   Fri Oct 16 12:43:26 2026 +0200

    android: Trigger roam events also when link capabilities/properties change

    This avoids issues in certain handover scenarios (e.g. mobile networks
    staying connected for a while when connecting to WiFi) where the
    previous two events are not triggered anymore (or only after a while).

diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java
index da1ed98533..2dccf496ec 100644
--- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java
+++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/NetworkManager.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2012-2019 Tobias Brunner
+ * Copyright (C) 2012-2026 Tobias Brunner
  *
  * Copyright (C) secunet Security Networks AG
  *
@@ -21,20 +21,28 @@ import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.net.ConnectivityManager;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
 import android.net.Network;
+import android.net.NetworkCapabilities;
 import android.net.NetworkRequest;
 import android.os.Build;

+import java.net.InetAddress;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;

+import androidx.annotation.NonNull;
+
 public class NetworkManager extends BroadcastReceiver implements Runnable
 {
 	private final Context mContext;
 	private volatile boolean mRegistered;
 	private ConnectivityManager.NetworkCallback mCallback;
 	private Thread mEventNotifier;
-	private int mConnectedNetworks = 0;
 	private LinkedList<Boolean> mEvents = new LinkedList<>();
+	private HashMap<Network, NetworkState> mNetworks = new HashMap<>();

 	public NetworkManager(Context context)
 	{
@@ -51,9 +59,8 @@ public class NetworkManager extends BroadcastReceiver implements Runnable
 					{
 						/* we expect this to be called if connected to at least one network during
 						 * callback registration */
-						mConnectedNetworks += 1;
-						mEvents.addLast(true);
-						NetworkManager.this.notifyAll();
+						mNetworks.put(network, new NetworkState());
+						notifyNetworkChanged();
 					}
 				}

@@ -63,9 +70,74 @@ public class NetworkManager extends BroadcastReceiver implements Runnable
 					synchronized (NetworkManager.this)
 					{
 						/* in particular mobile connections are disconnected overlapping with WiFi */
-						mConnectedNetworks -= 1;
-						mEvents.addLast(mConnectedNetworks > 0);
-						NetworkManager.this.notifyAll();
+						mNetworks.remove(network);
+						notifyNetworkChanged();
+					}
+				}
+
+				@Override
+				public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapabilities networkCapabilities)
+				{
+					synchronized (NetworkManager.this)
+					{
+						NetworkState state = mNetworks.get(network);
+						if (state != null)
+						{
+							int capabilities = 0;
+							/* only look at the capabilities relevant for us */
+							if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
+							{
+								capabilities |= (1 << 1);
+							}
+							if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P &&
+								networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED))
+							{
+								capabilities |= (1 << 2);
+							}
+							if (state.capabilities != capabilities)
+							{
+								state.capabilities = capabilities;
+								notifyNetworkChanged();
+							}
+						}
+					}
+				}
+
+				@Override
+				public void onLinkPropertiesChanged(@NonNull Network network, @NonNull LinkProperties linkProperties)
+				{
+					synchronized (NetworkManager.this)
+					{
+						NetworkState state = mNetworks.get(network);
+						if (state != null)
+						{
+							/* check addresses in case e.g. a new DHCP lease is assigned or
+							 * IPv4 arrives after IPv6 */
+							HashSet<InetAddress> addresses = new HashSet<>();
+							for (LinkAddress address : linkProperties.getLinkAddresses())
+							{
+								addresses.add(address.getAddress());
+							}
+							if (!state.addresses.equals(addresses))
+							{
+								state.addresses = addresses;
+								notifyNetworkChanged();
+							}
+						}
+					}
+				}
+
+				@Override
+				public void onBlockedStatusChanged(@NonNull Network network, boolean blocked)
+				{
+					synchronized (NetworkManager.this)
+					{
+						NetworkState state = mNetworks.get(network);
+						if (state != null && state.blocked != blocked)
+						{
+							state.blocked = blocked;
+							notifyNetworkChanged();
+						}
 					}
 				}
 			};
@@ -75,6 +147,7 @@ public class NetworkManager extends BroadcastReceiver implements Runnable
 	public void Register()
 	{
 		mEvents.clear();
+		mNetworks.clear();
 		mRegistered = true;
 		mEventNotifier = new Thread(this);
 		mEventNotifier.start();
@@ -149,6 +222,16 @@ public class NetworkManager extends BroadcastReceiver implements Runnable
 		}
 	}

+	/**
+	 * Queue a connectivity change event for the notifier thread.  Has to be
+	 * called from a synchronized() section.
+	 */
+	private void notifyNetworkChanged()
+	{
+		mEvents.addLast(!mNetworks.isEmpty());
+		NetworkManager.this.notifyAll();
+	}
+
 	@Override
 	public void run()
 	{
@@ -186,4 +269,14 @@ public class NetworkManager extends BroadcastReceiver implements Runnable
 	 * @param disconnected true if no connection is available at the moment
 	 */
 	public native void networkChanged(boolean disconnected);
+
+	/**
+	 * Simplified network state we track to see if anything changed
+	 */
+	private static class NetworkState
+	{
+		boolean blocked;
+		int capabilities;
+		HashSet<InetAddress> addresses = new HashSet<>();
+	}
 }