From 6c2647966b4423b6eb25ad221bdaf2f64bbb36b1 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:25:00 -0300 Subject: [PATCH 01/34] Refactor EngineRunner to not depend on VPNService --- .../io/netbird/client/tool/EngineRunner.java | 45 ++++++++++--------- .../io/netbird/client/tool/VPNService.java | 32 +++++++++---- 2 files changed, 47 insertions(+), 30 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index 55ec345..0c112ef 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -5,6 +5,9 @@ import android.os.Build; import android.util.Log; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import java.util.HashSet; import java.util.Set; @@ -12,55 +15,54 @@ import io.netbird.gomobile.android.Client; import io.netbird.gomobile.android.ConnectionListener; import io.netbird.gomobile.android.DNSList; +import io.netbird.gomobile.android.EnvList; import io.netbird.gomobile.android.NetworkArray; +import io.netbird.gomobile.android.NetworkChangeListener; import io.netbird.gomobile.android.PeerInfoArray; +import io.netbird.gomobile.android.TunAdapter; import io.netbird.gomobile.android.URLOpener; class EngineRunner { private static final String LOGTAG = "EngineRunner"; - private final Context context; private boolean engineIsRunning = false; Set serviceStateListeners = new HashSet<>(); private final Client goClient; - public EngineRunner(VPNService vpnService) { - context = vpnService; - NetworkChangeNotifier notifier = new NetworkChangeNotifier(vpnService); - IFace iFace = new IFace(vpnService); + public EngineRunner(String configurationFilePath, NetworkChangeListener networkChangeListener, TunAdapter tunAdapter, IFaceDiscover iFaceDiscover, String versionName, boolean isTraceLogEnabled, boolean isDebuggable) { goClient = Android.newClient( - Preferences.configFile(vpnService), + configurationFilePath, androidSDKVersion(), DeviceName.getDeviceName(), - Version.getVersionName(vpnService), - iFace, - new IFaceDiscover(), - notifier); + versionName, + tunAdapter, + iFaceDiscover, + networkChangeListener); - updateLogLevel(); + updateLogLevel(isTraceLogEnabled, isDebuggable); } - public void run(URLOpener urlOpener) { - runClient(urlOpener); + public void run(@NotNull DNSWatch dnsWatch, @NotNull Preferences preferences, boolean isDebuggable, @NotNull URLOpener urlOpener) { + runClient(dnsWatch, preferences, isDebuggable, urlOpener); } - public void runWithoutAuth() { - runClient(null); + public void runWithoutAuth(@NotNull DNSWatch dnsWatch, @NotNull Preferences preferences, boolean isDebuggable) { + runClient(dnsWatch, preferences, isDebuggable, null); } - private synchronized void runClient(URLOpener urlOpener) { + private synchronized void runClient(@NotNull DNSWatch dnsWatch, @NotNull Preferences preferences, boolean isDebuggable, @Nullable URLOpener urlOpener) { Log.d(LOGTAG, "run engine"); if (engineIsRunning) { Log.e(LOGTAG, "engine already running"); return; } - updateLogLevel(); + updateLogLevel(preferences.isTraceLogEnabled(), isDebuggable); engineIsRunning = true; Runnable r = () -> { - DNSWatch dnsWatch = new DNSWatch(context); - Preferences preferences = new Preferences(context); +// DNSWatch dnsWatch = new DNSWatch(context); +// Preferences preferences = new Preferences(context); var envList = EnvVarPackager.getEnvironmentVariables(preferences); try { @@ -145,9 +147,8 @@ private synchronized void notifyServiceStateListeners(boolean engineIsRunning) { } } - private void updateLogLevel() { - Preferences pref = new Preferences(context); - if (Version.isDebuggable(context) || pref.isTraceLogEnabled()) { + private void updateLogLevel(boolean isTraceLogEnabled , boolean isDebuggable) { + if (isDebuggable || isTraceLogEnabled) { goClient.setTraceLogLevel(); } else { goClient.setInfoLogLevel(); diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index 3d24785..4260ca8 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -11,6 +11,7 @@ import android.os.IBinder; import android.os.Parcel; import android.util.Log; + import androidx.annotation.Nullable; import io.netbird.gomobile.android.ConnectionListener; @@ -33,7 +34,17 @@ public class VPNService extends android.net.VpnService { public void onCreate() { super.onCreate(); Log.d(LOGTAG, "onCreate"); - engineRunner = new EngineRunner(this); + + var configurationFilePath = Preferences.configFile(this); + var versionName = Version.getVersionName(this); + var tunAdapter = new IFace(this); + var iFaceDiscover = new IFaceDiscover(); + var notifier = new NetworkChangeNotifier(this); + var preferences = new Preferences(this); + var isDebuggable = Version.isDebuggable(this); + + engineRunner = new EngineRunner(configurationFilePath, notifier, tunAdapter, iFaceDiscover, versionName, + preferences.isTraceLogEnabled(), isDebuggable); fgNotification = new ForegroundNotification(this); engineRunner.addServiceStateListener(serviceStateListener); } @@ -45,9 +56,12 @@ public int onStartCommand(@Nullable final Intent intent, final int flags, final return START_NOT_STICKY; } - if(INTENT_ALWAYS_ON_START.equals(intent.getAction())) { + if (INTENT_ALWAYS_ON_START.equals(intent.getAction())) { fgNotification.startForeground(); - engineRunner.runWithoutAuth(); + engineRunner.runWithoutAuth( + new DNSWatch(this), + new Preferences(this), + Version.isDebuggable(this)); } return super.onStartCommand(intent, flags, startId); } @@ -59,9 +73,9 @@ public IBinder onBind(Intent intent) { } @Override - public boolean onUnbind (Intent intent) { + public boolean onUnbind(Intent intent) { Log.d(LOGTAG, "unbind from activity"); - if(!engineRunner.isRunning()) { + if (!engineRunner.isRunning()) { stopSelf(); } return false; // false means do not call onRebind @@ -78,7 +92,7 @@ public void onDestroy() { @Override public void onRevoke() { Log.d(LOGTAG, "VPN permission on revoke"); - if(engineRunner!=null) { + if (engineRunner != null) { engineRunner.stop(); stopForeground(true); } @@ -104,7 +118,9 @@ public Intent prepareVpnIntent(Activity context) { public void runEngine(URLOpener urlOpener) { fgNotification.startForeground(); - engineRunner.run(urlOpener); + engineRunner.run(new DNSWatch(VPNService.this), + new Preferences(VPNService.this), + Version.isDebuggable(VPNService.this), urlOpener); } public void stopEngine() { @@ -135,7 +151,7 @@ public void removeServiceStateListener(ServiceStateListener serviceStateListener engineRunner.removeServiceStateListener(serviceStateListener); } } - + public static boolean isUsingAlwaysOnVPN(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); Network[] networks = connectivityManager.getAllNetworks(); From 03179debd8076fdba746a3fb74c8dd7fcfcc0d32 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:25:33 -0300 Subject: [PATCH 02/34] Remove unused imports --- tool/src/main/java/io/netbird/client/tool/EngineRunner.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index 0c112ef..6ababf6 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -1,7 +1,5 @@ package io.netbird.client.tool; - -import android.content.Context; import android.os.Build; import android.util.Log; @@ -15,7 +13,6 @@ import io.netbird.gomobile.android.Client; import io.netbird.gomobile.android.ConnectionListener; import io.netbird.gomobile.android.DNSList; -import io.netbird.gomobile.android.EnvList; import io.netbird.gomobile.android.NetworkArray; import io.netbird.gomobile.android.NetworkChangeListener; import io.netbird.gomobile.android.PeerInfoArray; From 05e219504f8f00e55a4b3ca807744ba60c3b8b46 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 08:41:51 -0300 Subject: [PATCH 03/34] Send received routesString as extra on broadcast intent --- .../java/io/netbird/client/tool/NetworkChangeNotifier.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java index 7cbc826..87d9802 100644 --- a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java +++ b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java @@ -19,7 +19,7 @@ public class NetworkChangeNotifier implements NetworkChangeListener { @Override public void onNetworkChanged(String routes) { - sendBroadcast(); + sendBroadcast(routes); } @Override @@ -27,9 +27,10 @@ public void setInterfaceIP(String ip) { } - private void sendBroadcast() { + private void sendBroadcast(String routes) { Intent intent = new Intent(action); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("routes", routes); LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } } \ No newline at end of file From 5f89ae6f436dcb351bc9d9e3b17e064175d2d884 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 08:43:09 -0300 Subject: [PATCH 04/34] Add TUNParameters to hold last used vpn parameters --- .../io/netbird/client/tool/TUNParameters.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 tool/src/main/java/io/netbird/client/tool/TUNParameters.java diff --git a/tool/src/main/java/io/netbird/client/tool/TUNParameters.java b/tool/src/main/java/io/netbird/client/tool/TUNParameters.java new file mode 100644 index 0000000..e5d7257 --- /dev/null +++ b/tool/src/main/java/io/netbird/client/tool/TUNParameters.java @@ -0,0 +1,25 @@ +package io.netbird.client.tool; + +public class TUNParameters { + String address; + long mtu; + String dns; + String searchDomainsString; + String routesString; + + public TUNParameters(String address, long mtu, String dns, String searchDomainsString, String routesString) { + this.address = address; + this.mtu = mtu; + this.dns = dns; + this.searchDomainsString = searchDomainsString; + this.routesString = routesString; + } + + public boolean didRoutesChange(String routesString) { + if (this.routesString != null) { + return !this.routesString.equals(routesString); + } else { + return routesString != null; + } + } +} From fdde77bce69a9599433bab3bbf206c4dc61570bd Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 08:46:20 -0300 Subject: [PATCH 05/34] Add method to trigger renewal of TUN file descriptor on client --- .../java/io/netbird/client/tool/EngineRunner.java | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index 6ababf6..afbf675 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -58,13 +58,11 @@ private synchronized void runClient(@NotNull DNSWatch dnsWatch, @NotNull Prefere engineIsRunning = true; Runnable r = () -> { -// DNSWatch dnsWatch = new DNSWatch(context); -// Preferences preferences = new Preferences(context); var envList = EnvVarPackager.getEnvironmentVariables(preferences); try { notifyServiceStateListeners(true); - if(urlOpener == null) { + if (urlOpener == null) { goClient.runWithoutLogin(dnsWatch.dnsServers(), () -> dnsWatch.setDNSChangeListener(this::changed), envList); } else { goClient.run(urlOpener, dnsWatch.dnsServers(), () -> dnsWatch.setDNSChangeListener(this::changed), envList); @@ -86,6 +84,7 @@ private synchronized void runClient(@NotNull DNSWatch dnsWatch, @NotNull Prefere private void changed(DNSList dnsServers) throws Exception { goClient.onUpdatedHostDNS(dnsServers); } + public synchronized boolean isRunning() { return engineIsRunning; } @@ -144,7 +143,7 @@ private synchronized void notifyServiceStateListeners(boolean engineIsRunning) { } } - private void updateLogLevel(boolean isTraceLogEnabled , boolean isDebuggable) { + private void updateLogLevel(boolean isTraceLogEnabled, boolean isDebuggable) { if (isDebuggable || isTraceLogEnabled) { goClient.setTraceLogLevel(); } else { @@ -153,6 +152,11 @@ private void updateLogLevel(boolean isTraceLogEnabled , boolean isDebuggable) { } private int androidSDKVersion() { - return Build.VERSION.SDK_INT ; + return Build.VERSION.SDK_INT; + } + + public void renewTUN(int fd) { + Log.d(LOGTAG, "renewing TUN fd"); + // goClient.sendFd(fd); } } From c63d3d61e592b0f7cf69aa1b04fed96ddf21af10 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 08:48:51 -0300 Subject: [PATCH 06/34] Add call to VPNService to save last used parameters when successfully creating a TUN --- .../main/java/io/netbird/client/tool/IFace.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java index 37d8ff8..918d2ac 100644 --- a/tool/src/main/java/io/netbird/client/tool/IFace.java +++ b/tool/src/main/java/io/netbird/client/tool/IFace.java @@ -33,12 +33,20 @@ public long configureInterface(String address, long mtu, String dns, String sear LinkedList routes = toRoutes(routesString); InetNetwork addr = InetNetwork.parse(address); + long fd = -1; + try { - return createTun(addr.getAddress().getHostAddress(), addr.getMask(), (int) mtu, dns, searchDomains, routes); - }catch (Exception e) { + fd = createTun(addr.getAddress().getHostAddress(), addr.getMask(), (int) mtu, dns, searchDomains, routes); + } catch (Exception e) { Log.e(LOGTAG, "failed to create tunnel", e); - throw e; } + + // only set the currently used TUN parameters if createTun didn't throw exceptions + if (fd != -1) { + this.vpnService.setCurrentTUNParameters(new TUNParameters(address, mtu, dns, searchDomainsString, routesString)); + } + + return fd; } @Override From fe5dd6544d44088869f746e01294deb47e6abb00 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 08:55:00 -0300 Subject: [PATCH 07/34] Replace ',' with ';' on onNetworkChanged call The marshalled routes are being sent from go client separated by semicolons when provided in the IFace configureInterface callback, but separated by commas on onNetworkChanged call --- .../java/io/netbird/client/tool/NetworkChangeNotifier.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java index 87d9802..74589d8 100644 --- a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java +++ b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java @@ -19,6 +19,10 @@ public class NetworkChangeNotifier implements NetworkChangeListener { @Override public void onNetworkChanged(String routes) { + if (routes != null) { + routes = routes.replace(",", ";"); + } + sendBroadcast(routes); } From f232a6f4c9219cab431005ebacc7bda3cdcd64fb Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 08:58:41 -0300 Subject: [PATCH 08/34] Add check on current looper when adding dns to VPNService.builder --- .../java/io/netbird/client/tool/IFace.java | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java index 918d2ac..e41a280 100644 --- a/tool/src/main/java/io/netbird/client/tool/IFace.java +++ b/tool/src/main/java/io/netbird/client/tool/IFace.java @@ -104,25 +104,33 @@ private void prepareDnsSetting(VpnService.Builder builder, String dns) { return; } - CountDownLatch latch = new CountDownLatch(1); + if (Looper.myLooper() != Looper.getMainLooper()) { + CountDownLatch latch = new CountDownLatch(1); - // ConnectivityManager must to run on the main thread instead of a Go routine - new Handler(Looper.getMainLooper()).post(() -> { - DNSWatch dnsWatch = new DNSWatch(vpnService); + // ConnectivityManager must to run on the main thread instead of a Go routine + new Handler(Looper.getMainLooper()).post(() -> { + addDnsServer(builder, dns); - if (!dnsWatch.isPrivateDnsActive()) { - builder.addDnsServer(dns); - } else { - Log.d(LOGTAG, "ignore DNS because private dns is active"); + latch.countDown(); + }); + + try { + latch.await(); // Will block the current thread until countDown() is called + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } + } else { + addDnsServer(builder, dns); + } + } - latch.countDown(); - }); + private void addDnsServer(VpnService.Builder builder, String dns) { + DNSWatch dnsWatch = new DNSWatch(vpnService); - try { - latch.await(); // Will block the current thread until countDown() is called - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + if (!dnsWatch.isPrivateDnsActive()) { + builder.addDnsServer(dns); + } else { + Log.d(LOGTAG, "ignore DNS because private dns is active"); } } From 4bfb241508af0802be1232295da83e0f1f7c01b2 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 09:00:08 -0300 Subject: [PATCH 09/34] Add broadcast receiver to VPNService to recreate TUN --- .../io/netbird/client/tool/VPNService.java | 45 ++++++++++++++++++- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index 4260ca8..2e2d8aa 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -1,8 +1,10 @@ package io.netbird.client.tool; import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; @@ -13,6 +15,8 @@ import android.util.Log; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import io.netbird.gomobile.android.ConnectionListener; import io.netbird.gomobile.android.NetworkArray; @@ -24,17 +28,49 @@ public class VPNService extends android.net.VpnService { private final static String LOGTAG = "service"; public static final String INTENT_ACTION_START = "io.netbird.client.intent.action.START_SERVICE"; private static final String INTENT_ALWAYS_ON_START = "android.net.VpnService"; - private final IBinder myBinder = new MyLocalBinder(); - private EngineRunner engineRunner; private ForegroundNotification fgNotification; + private final BroadcastReceiver networkRouteChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() != null && intent.getAction().equals(NetworkChangeNotifier.action)) { + String routes = intent.getStringExtra("routes"); + + // Renew TUN file descriptor if routes changed. + if (currentTUNParameters != null && currentTUNParameters.didRoutesChange(routes)) { + var iface = new IFace(VPNService.this); + + try { + int fd = (int)iface.configureInterface( + currentTUNParameters.address, + currentTUNParameters.mtu, + currentTUNParameters.dns, + currentTUNParameters.searchDomainsString, + routes); + + if (fd != -1) { + VPNService.this.protect(fd); + VPNService.this.engineRunner.renewTUN(fd); + } + } catch (Exception e) { + Log.e(LOGTAG, "failed to recreate tunnel after route changed", e); + } + } + } + } + }; + private TUNParameters currentTUNParameters; @Override public void onCreate() { super.onCreate(); Log.d(LOGTAG, "onCreate"); + LocalBroadcastManager + .getInstance(this) + .registerReceiver(networkRouteChangedReceiver, new IntentFilter(NetworkChangeNotifier.action)); + var configurationFilePath = Preferences.configFile(this); var versionName = Version.getVersionName(this); var tunAdapter = new IFace(this); @@ -87,6 +123,7 @@ public void onDestroy() { Log.d(LOGTAG, "onDestroy"); engineRunner.stop(); stopForeground(true); + LocalBroadcastManager.getInstance(this).unregisterReceiver(networkRouteChangedReceiver); } @Override @@ -185,4 +222,8 @@ public void onError(String msg) { fgNotification.stopForeground(); } }; + + public void setCurrentTUNParameters(TUNParameters currentTUNParameters) { + this.currentTUNParameters = currentTUNParameters; + } } From 7b320845502764845407f6b326473ae9256a2212 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 09:03:40 -0300 Subject: [PATCH 10/34] Remove unused import --- tool/src/main/java/io/netbird/client/tool/VPNService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index 2e2d8aa..8106637 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -15,7 +15,6 @@ import android.util.Log; import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import io.netbird.gomobile.android.ConnectionListener; From 6056bb67b15898738e41031f470aec5e6f47c289 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:54:08 -0300 Subject: [PATCH 11/34] Change log formatting --- tool/src/main/java/io/netbird/client/tool/EngineRunner.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index afbf675..39aacd2 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -156,7 +156,7 @@ private int androidSDKVersion() { } public void renewTUN(int fd) { - Log.d(LOGTAG, "renewing TUN fd"); + Log.d(LOGTAG, String.format("renewing TUN fd: %d", fd)); // goClient.sendFd(fd); } } From 88699ed8e49179f7da0f7c9b69832fed8c154bd2 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:56:00 -0300 Subject: [PATCH 12/34] Remove usage of BroadcastReceiver on VPNService Now VPNService adds an OnRouteChanged listener to NetworkChangeNotifier, which queues TUN renewal via a looper thread --- .../client/tool/NetworkChangeNotifier.java | 10 ++ .../client/tool/RouteChangeListener.java | 5 + .../client/tool/TUNCreatorLooperThread.java | 54 +++++++++++ .../io/netbird/client/tool/VPNService.java | 92 +++++++++++-------- 4 files changed, 124 insertions(+), 37 deletions(-) create mode 100644 tool/src/main/java/io/netbird/client/tool/RouteChangeListener.java create mode 100644 tool/src/main/java/io/netbird/client/tool/TUNCreatorLooperThread.java diff --git a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java index 74589d8..c1708b5 100644 --- a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java +++ b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java @@ -13,6 +13,8 @@ public class NetworkChangeNotifier implements NetworkChangeListener { private final Context context; + private RouteChangeListener routeChangeListener; + NetworkChangeNotifier(Context context) { this.context = context; } @@ -23,6 +25,10 @@ public void onNetworkChanged(String routes) { routes = routes.replace(",", ";"); } + if (this.routeChangeListener != null) { + this.routeChangeListener.onRouteChanged(routes); + } + sendBroadcast(routes); } @@ -31,6 +37,10 @@ public void setInterfaceIP(String ip) { } + public void setRouteChangeListener(RouteChangeListener routeChangeListener) { + this.routeChangeListener = routeChangeListener; + } + private void sendBroadcast(String routes) { Intent intent = new Intent(action); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/tool/src/main/java/io/netbird/client/tool/RouteChangeListener.java b/tool/src/main/java/io/netbird/client/tool/RouteChangeListener.java new file mode 100644 index 0000000..8a01901 --- /dev/null +++ b/tool/src/main/java/io/netbird/client/tool/RouteChangeListener.java @@ -0,0 +1,5 @@ +package io.netbird.client.tool; + +public interface RouteChangeListener { + void onRouteChanged(String routes); +} diff --git a/tool/src/main/java/io/netbird/client/tool/TUNCreatorLooperThread.java b/tool/src/main/java/io/netbird/client/tool/TUNCreatorLooperThread.java new file mode 100644 index 0000000..eeca20a --- /dev/null +++ b/tool/src/main/java/io/netbird/client/tool/TUNCreatorLooperThread.java @@ -0,0 +1,54 @@ +package io.netbird.client.tool; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.core.util.Consumer; + +import java.util.Objects; + +public class TUNCreatorLooperThread extends Thread { + private static final String TAG = TUNCreatorLooperThread.class.getSimpleName(); + private Handler handler; + + private final Consumer tunCreator; + + public TUNCreatorLooperThread(Consumer tunCreator) { + this.tunCreator = tunCreator; + } + + public void run() { + Looper.prepare(); + + synchronized (this) { + handler = new Handler(Objects.requireNonNull(Looper.myLooper())) { + @Override + public void handleMessage(@NonNull Message msg) { + if (msg.what == 1) { + Log.d(TAG, "handleMessage: renewing TUN!"); + String routes = msg.obj.toString(); + tunCreator.accept(routes); + } + } + }; + notifyAll(); + } + + Looper.loop(); + } + + public synchronized Handler getHandler() { + while (handler == null) { + try { + wait(); + } catch (InterruptedException e) { + Log.d(TAG, "getHandler: ", e); + } + } + + return handler; + } +} diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index 8106637..b300c14 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -1,21 +1,22 @@ package io.netbird.client.tool; import android.app.Activity; -import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; import android.net.VpnService; import android.os.Binder; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.os.Parcel; import android.util.Log; import androidx.annotation.Nullable; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import java.util.Random; import io.netbird.gomobile.android.ConnectionListener; import io.netbird.gomobile.android.NetworkArray; @@ -30,35 +31,6 @@ public class VPNService extends android.net.VpnService { private final IBinder myBinder = new MyLocalBinder(); private EngineRunner engineRunner; private ForegroundNotification fgNotification; - private final BroadcastReceiver networkRouteChangedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction() != null && intent.getAction().equals(NetworkChangeNotifier.action)) { - String routes = intent.getStringExtra("routes"); - - // Renew TUN file descriptor if routes changed. - if (currentTUNParameters != null && currentTUNParameters.didRoutesChange(routes)) { - var iface = new IFace(VPNService.this); - - try { - int fd = (int)iface.configureInterface( - currentTUNParameters.address, - currentTUNParameters.mtu, - currentTUNParameters.dns, - currentTUNParameters.searchDomainsString, - routes); - - if (fd != -1) { - VPNService.this.protect(fd); - VPNService.this.engineRunner.renewTUN(fd); - } - } catch (Exception e) { - Log.e(LOGTAG, "failed to recreate tunnel after route changed", e); - } - } - } - } - }; private TUNParameters currentTUNParameters; @Override @@ -66,15 +38,14 @@ public void onCreate() { super.onCreate(); Log.d(LOGTAG, "onCreate"); - LocalBroadcastManager - .getInstance(this) - .registerReceiver(networkRouteChangedReceiver, new IntentFilter(NetworkChangeNotifier.action)); - var configurationFilePath = Preferences.configFile(this); var versionName = Version.getVersionName(this); var tunAdapter = new IFace(this); var iFaceDiscover = new IFaceDiscover(); + var notifier = new NetworkChangeNotifier(this); + notifier.setRouteChangeListener((routes -> queueTUNRenewal(routes))); + var preferences = new Preferences(this); var isDebuggable = Version.isDebuggable(this); @@ -122,7 +93,11 @@ public void onDestroy() { Log.d(LOGTAG, "onDestroy"); engineRunner.stop(); stopForeground(true); - LocalBroadcastManager.getInstance(this).unregisterReceiver(networkRouteChangedReceiver); + + if (tunCreator != null) { + tunCreator.getHandler().getLooper().quitSafely(); + tunCreator = null; + } } @Override @@ -222,6 +197,49 @@ public void onError(String msg) { } }; + private TUNCreatorLooperThread tunCreator; + + private void queueTUNRenewal(String routes) { + if (tunCreator == null) { + tunCreator = new TUNCreatorLooperThread(this::recreateTUN); + tunCreator.setPriority(Thread.MAX_PRIORITY); + tunCreator.start(); + } + + var message = tunCreator.getHandler().obtainMessage(1, routes); + boolean isQueued = tunCreator.getHandler().sendMessage(message); + + Log.d(LOGTAG, String.format("is TUN renewal queued? %b", isQueued)); + } + + private void recreateTUN(String routes) { + if (!engineRunner.isRunning()) return; + + // Renew TUN file descriptor if routes changed. + if (currentTUNParameters != null && currentTUNParameters.didRoutesChange(routes)) { + int fd = new Random().nextInt(); + this.engineRunner.renewTUN(fd); + +// var iface = new IFace(VPNService.this); +// +// try { +// int fd = (int)iface.configureInterface( +// currentTUNParameters.address, +// currentTUNParameters.mtu, +// currentTUNParameters.dns, +// currentTUNParameters.searchDomainsString, +// routes); +// +// if (fd != -1) { +// this.protect(fd); +// this.engineRunner.renewTUN(fd); +// } +// } catch (Exception e) { +// Log.e(LOGTAG, "failed to recreate tunnel after route changed", e); +// } + } + } + public void setCurrentTUNParameters(TUNParameters currentTUNParameters) { this.currentTUNParameters = currentTUNParameters; } From 4539db827424d11af0498111965a6525c064cb84 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:56:13 -0300 Subject: [PATCH 13/34] Revert "Add check on current looper when adding dns to VPNService.builder" This reverts commit f232a6f4c9219cab431005ebacc7bda3cdcd64fb. --- .../java/io/netbird/client/tool/IFace.java | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/IFace.java b/tool/src/main/java/io/netbird/client/tool/IFace.java index e41a280..918d2ac 100644 --- a/tool/src/main/java/io/netbird/client/tool/IFace.java +++ b/tool/src/main/java/io/netbird/client/tool/IFace.java @@ -104,33 +104,25 @@ private void prepareDnsSetting(VpnService.Builder builder, String dns) { return; } - if (Looper.myLooper() != Looper.getMainLooper()) { - CountDownLatch latch = new CountDownLatch(1); + CountDownLatch latch = new CountDownLatch(1); - // ConnectivityManager must to run on the main thread instead of a Go routine - new Handler(Looper.getMainLooper()).post(() -> { - addDnsServer(builder, dns); + // ConnectivityManager must to run on the main thread instead of a Go routine + new Handler(Looper.getMainLooper()).post(() -> { + DNSWatch dnsWatch = new DNSWatch(vpnService); - latch.countDown(); - }); - - try { - latch.await(); // Will block the current thread until countDown() is called - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + if (!dnsWatch.isPrivateDnsActive()) { + builder.addDnsServer(dns); + } else { + Log.d(LOGTAG, "ignore DNS because private dns is active"); } - } else { - addDnsServer(builder, dns); - } - } - private void addDnsServer(VpnService.Builder builder, String dns) { - DNSWatch dnsWatch = new DNSWatch(vpnService); + latch.countDown(); + }); - if (!dnsWatch.isPrivateDnsActive()) { - builder.addDnsServer(dns); - } else { - Log.d(LOGTAG, "ignore DNS because private dns is active"); + try { + latch.await(); // Will block the current thread until countDown() is called + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } } From 6af40159bd1aca4f7c5986ac40ac4d47fe647838 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Tue, 23 Sep 2025 18:39:00 -0300 Subject: [PATCH 14/34] Add call to goClient.renewTun function when renewing TUN --- .../io/netbird/client/tool/EngineRunner.java | 7 ++- .../io/netbird/client/tool/VPNService.java | 45 ++++++++++--------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index 39aacd2..7a512a6 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -157,6 +157,11 @@ private int androidSDKVersion() { public void renewTUN(int fd) { Log.d(LOGTAG, String.format("renewing TUN fd: %d", fd)); - // goClient.sendFd(fd); + try { + goClient.renewTun(fd); + } catch (Exception e) { + Log.e(LOGTAG, "goClient error", e); + notifyError(e); + } } } diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index b300c14..c0b234a 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -20,6 +20,7 @@ import io.netbird.gomobile.android.ConnectionListener; import io.netbird.gomobile.android.NetworkArray; +import io.netbird.gomobile.android.NetworkChangeListener; import io.netbird.gomobile.android.PeerInfoArray; import io.netbird.gomobile.android.URLOpener; @@ -32,6 +33,7 @@ public class VPNService extends android.net.VpnService { private EngineRunner engineRunner; private ForegroundNotification fgNotification; private TUNParameters currentTUNParameters; + private NetworkChangeNotifier notifier; @Override public void onCreate() { @@ -43,7 +45,7 @@ public void onCreate() { var tunAdapter = new IFace(this); var iFaceDiscover = new IFaceDiscover(); - var notifier = new NetworkChangeNotifier(this); + notifier = new NetworkChangeNotifier(this); notifier.setRouteChangeListener((routes -> queueTUNRenewal(routes))); var preferences = new Preferences(this); @@ -94,6 +96,10 @@ public void onDestroy() { engineRunner.stop(); stopForeground(true); + if (this.notifier != null) { + this.notifier.setRouteChangeListener(null); + } + if (tunCreator != null) { tunCreator.getHandler().getLooper().quitSafely(); tunCreator = null; @@ -217,26 +223,23 @@ private void recreateTUN(String routes) { // Renew TUN file descriptor if routes changed. if (currentTUNParameters != null && currentTUNParameters.didRoutesChange(routes)) { - int fd = new Random().nextInt(); - this.engineRunner.renewTUN(fd); - -// var iface = new IFace(VPNService.this); -// -// try { -// int fd = (int)iface.configureInterface( -// currentTUNParameters.address, -// currentTUNParameters.mtu, -// currentTUNParameters.dns, -// currentTUNParameters.searchDomainsString, -// routes); -// -// if (fd != -1) { -// this.protect(fd); -// this.engineRunner.renewTUN(fd); -// } -// } catch (Exception e) { -// Log.e(LOGTAG, "failed to recreate tunnel after route changed", e); -// } + var iface = new IFace(VPNService.this); + + try { + int fd = (int)iface.configureInterface( + currentTUNParameters.address, + currentTUNParameters.mtu, + currentTUNParameters.dns, + currentTUNParameters.searchDomainsString, + routes); + + if (fd != -1) { + this.protect(fd); + this.engineRunner.renewTUN(fd); + } + } catch (Exception e) { + Log.e(LOGTAG, "failed to recreate tunnel after route changed", e); + } } } From 32bfe6954fbbc286ce8833c7cbe3c47b54053023 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:07:17 -0300 Subject: [PATCH 15/34] Add ViewModel to NetworksFragment --- .../java/io/netbird/client/MyApplication.java | 5 ++ .../repository/VPNServiceBindListener.java | 5 ++ .../repository/VPNServiceRepository.java | 78 +++++++++++++++++++ .../client/ui/home/NetworksFragment.java | 76 ++++++++++++++---- .../ui/home/NetworksFragmentUiState.java | 15 ++++ .../ui/home/NetworksFragmentViewModel.java | 56 +++++++++++++ 6 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java create mode 100644 app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java create mode 100644 app/src/main/java/io/netbird/client/ui/home/NetworksFragmentUiState.java create mode 100644 app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java diff --git a/app/src/main/java/io/netbird/client/MyApplication.java b/app/src/main/java/io/netbird/client/MyApplication.java index 0af6e16..462c3a4 100644 --- a/app/src/main/java/io/netbird/client/MyApplication.java +++ b/app/src/main/java/io/netbird/client/MyApplication.java @@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatDelegate; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import io.netbird.client.repository.VPNServiceRepository; import io.netbird.client.tool.NetworkChangeNotifier; public class MyApplication extends Application { @@ -28,4 +29,8 @@ public void registerNetworkReceiver() { new IntentFilter(NetworkChangeNotifier.action) ); } + + public VPNServiceRepository getVPNServiceRepository() { + return new VPNServiceRepository(this); + } } \ No newline at end of file diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java b/app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java new file mode 100644 index 0000000..98f5fa3 --- /dev/null +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java @@ -0,0 +1,5 @@ +package io.netbird.client.repository; + +public interface VPNServiceBindListener { + void onBind(); +} diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java new file mode 100644 index 0000000..98d9d9d --- /dev/null +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java @@ -0,0 +1,78 @@ +package io.netbird.client.repository; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; + +import java.util.ArrayList; +import java.util.List; + +import io.netbird.client.tool.VPNService; +import io.netbird.client.ui.home.Resource; +import io.netbird.client.ui.home.Status; + +public class VPNServiceRepository { + private VPNService.MyLocalBinder binder; + private final Context context; + private VPNServiceBindListener listener; + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + binder = (VPNService.MyLocalBinder)service; + if (listener != null) { + listener.onBind(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + if (binder != null) { + binder = null; + } + + listener = null; + } + }; + + public VPNServiceRepository(Context context) { + this.context = context; + } + + public void setListener(VPNServiceBindListener listener) { + this.listener = listener; + } + + public void bindService() { + var intent = new Intent(context, VPNService.class); + context.bindService(intent, serviceConnection, Context.BIND_ABOVE_CLIENT); + } + + public void unbindService() { + if (binder != null) { + context.unbindService(serviceConnection); + binder = null; + } + } + + public List getNetworks() { + if (binder == null) { + return new ArrayList<>(); + } + + var resources = new ArrayList(); + var networks = binder.networks(); + + for (int i = 0; i < networks.size(); i++) { + var network = networks.get(i); + resources.add(new Resource(Status.fromString(network.getStatus()), + network.getName(), + network.getNetwork(), + network.getPeer())); + } + + return resources; + } +} diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java index a651aea..c44e796 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java @@ -3,9 +3,11 @@ import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.Looper; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -15,24 +17,26 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import java.util.ArrayList; +import java.util.List; import io.netbird.client.R; import io.netbird.client.ServiceAccessor; import io.netbird.client.databinding.FragmentNetworksBinding; import io.netbird.gomobile.android.Network; import io.netbird.gomobile.android.NetworkArray; -import io.netbird.gomobile.android.PeerInfo; -import io.netbird.gomobile.android.PeerInfoArray; public class NetworksFragment extends Fragment { private FragmentNetworksBinding binding; private ServiceAccessor serviceAccessor; - private RecyclerView resourcesListView; + private RecyclerView resourcesRecyclerView; + private NetworksAdapter adapter; + private List resources = new ArrayList<>(); public static NetworksFragment newInstance() { return new NetworksFragment(); @@ -60,25 +64,41 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + var model = new ViewModelProvider(this, + ViewModelProvider.Factory.from(NetworksFragmentViewModel.initializer)) + .get(NetworksFragmentViewModel.class); + +// ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.networksList, !resources.isEmpty()); ZeroPeerView.setupLearnWhyClick(binding.zeroPeerLayout, requireContext()); - NetworkArray networks = serviceAccessor.getNetworks(); - updateNetworkCount(networks); + adapter = new NetworksAdapter(resources); - ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.networksList, networks.size() > 0); + resourcesRecyclerView = binding.networksRecyclerView; + resourcesRecyclerView.setAdapter(adapter); + resourcesRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); - resourcesListView = binding.networksRecyclerView; - resourcesListView.setLayoutManager(new LinearLayoutManager(requireContext())); + model.getUiState().observe(getViewLifecycleOwner(), uiState -> { + resources.clear(); + resources.addAll(uiState.getResources()); - ArrayList resources = new ArrayList<>(); - for( int i = 0; i < networks.size(); i++) { - Network network = networks.get(i); - Status status = Status.fromString(network.getStatus()); - resources.add(new Resource(status, network.getName(), network.getNetwork(), network.getPeer())); - } + updateResourcesCounter(resources); + ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.networksList, !resources.isEmpty()); +// Log.d(NetworksFragment.class.getSimpleName(), "observing viewModel. Is it running on UI thread? " +// + Looper.getMainLooper().equals(Looper.myLooper())); + adapter.notifyDataSetChanged(); + }); - NetworksAdapter adapter = new NetworksAdapter(resources); - resourcesListView.setAdapter(adapter); +// NetworkArray networks = serviceAccessor.getNetworks(); +// updateNetworkCount(networks); + + // ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.networksList, networks.size() > 0); + +// ArrayList resources = new ArrayList<>(); +// for( int i = 0; i < networks.size(); i++) { +// Network network = networks.get(i); +// Status status = Status.fromString(network.getStatus()); +// resources.add(new Resource(status, network.getName(), network.getNetwork(), network.getPeer())); +// } binding.searchView.clearFocus(); binding.searchView.addTextChangedListener(new TextWatcher() { @@ -99,6 +119,30 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat }); } + private void updateResourcesCounter(List resources) { + TextView textPeersCount = binding.textOpenPanel; + int connected = 0; + + for (var resource : resources) { + if (resource.getStatus().equals(Status.CONNECTED)) { + connected++; + } + } + +// for(int i = 0; i < resources.size(); i++) { +// Network network = networks.get(i); +// Status status = Status.fromString(network.getStatus()); +// if (status.equals(Status.CONNECTED)) { +// connected++; +// } +// } + + String text = getString(R.string.resources_connected, connected, resources.size()); + textPeersCount.post(() -> + textPeersCount.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)) + ); + } + private void updateNetworkCount(NetworkArray networks) { TextView textPeersCount = binding.textOpenPanel; int connected = 0; diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentUiState.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentUiState.java new file mode 100644 index 0000000..bef16df --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentUiState.java @@ -0,0 +1,15 @@ +package io.netbird.client.ui.home; + +import java.util.List; + +public class NetworksFragmentUiState { + private final List resources; + + public NetworksFragmentUiState(List resources) { + this.resources = resources; + } + + public List getResources() { + return resources; + } +} diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java new file mode 100644 index 0000000..11642fb --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java @@ -0,0 +1,56 @@ +package io.netbird.client.ui.home; + +import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.viewmodel.ViewModelInitializer; + +import java.util.ArrayList; + +import io.netbird.client.MyApplication; +import io.netbird.client.repository.VPNServiceBindListener; +import io.netbird.client.repository.VPNServiceRepository; + +public class NetworksFragmentViewModel extends ViewModel implements VPNServiceBindListener { + private final VPNServiceRepository repository; + private final MutableLiveData uiState = + new MutableLiveData<>(new NetworksFragmentUiState(new ArrayList<>())); + + public NetworksFragmentViewModel(VPNServiceRepository repository) { + this.repository = repository; + this.repository.setListener(this); + this.repository.bindService(); + } + + @Override + protected void onCleared() { + super.onCleared(); + repository.unbindService(); + } + + public LiveData getUiState() { + return uiState; + } + + public void getResources() { + var resources = repository.getNetworks(); + + uiState.setValue(new NetworksFragmentUiState(resources)); + } + + static final ViewModelInitializer initializer = new ViewModelInitializer<>( + NetworksFragmentViewModel.class, + creationExtras -> { + MyApplication app = (MyApplication) creationExtras.get(APPLICATION_KEY); + assert app != null; + return new NetworksFragmentViewModel(app.getVPNServiceRepository()); + } + ); + + @Override + public void onBind() { + getResources(); + } +} From c44ff830a3b83719d4eca5439e850d8f6de78fec Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:51:05 -0300 Subject: [PATCH 16/34] Reapply filtering to adapter after ui state is refreshed --- .../main/java/io/netbird/client/ui/home/NetworksFragment.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java index c44e796..5228018 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java @@ -59,7 +59,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c return binding.getRoot(); } - @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); @@ -83,9 +82,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat updateResourcesCounter(resources); ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.networksList, !resources.isEmpty()); -// Log.d(NetworksFragment.class.getSimpleName(), "observing viewModel. Is it running on UI thread? " -// + Looper.getMainLooper().equals(Looper.myLooper())); adapter.notifyDataSetChanged(); + adapter.filterBySearchQuery(binding.searchView.getText().toString()); }); // NetworkArray networks = serviceAccessor.getNetworks(); From b1631d5a92e0d1e50aa0b04fb27650d7187cd8fd Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:55:46 -0300 Subject: [PATCH 17/34] Remove old code --- netbird | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbird b/netbird index 2c87fa6..b2fbdfa 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit 2c87fa623654c5eef76bc0226062290201eef13a +Subproject commit b2fbdfabeecf9c76af84847ba3e36492317799bc From 60bb0e2933cc77e29c4c76ea9848a985f2020289 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:58:44 -0300 Subject: [PATCH 18/34] Remove old code Also remove usage of ServiceAccessor from NetworksFragment --- .../client/ui/home/NetworksFragment.java | 54 +------------------ 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java index 5228018..0aea246 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java @@ -33,25 +33,13 @@ public class NetworksFragment extends Fragment { private FragmentNetworksBinding binding; - private ServiceAccessor serviceAccessor; - private RecyclerView resourcesRecyclerView; private NetworksAdapter adapter; - private List resources = new ArrayList<>(); + private final List resources = new ArrayList<>(); public static NetworksFragment newInstance() { return new NetworksFragment(); } - @Override - public void onAttach(@NonNull Context context) { - super.onAttach(context); - if (context instanceof ServiceAccessor) { - serviceAccessor = (ServiceAccessor) context; - } else { - throw new RuntimeException(context + " must implement ServiceAccessor"); - } - } - @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -67,12 +55,11 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat ViewModelProvider.Factory.from(NetworksFragmentViewModel.initializer)) .get(NetworksFragmentViewModel.class); -// ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.networksList, !resources.isEmpty()); ZeroPeerView.setupLearnWhyClick(binding.zeroPeerLayout, requireContext()); adapter = new NetworksAdapter(resources); - resourcesRecyclerView = binding.networksRecyclerView; + RecyclerView resourcesRecyclerView = binding.networksRecyclerView; resourcesRecyclerView.setAdapter(adapter); resourcesRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); @@ -86,18 +73,6 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat adapter.filterBySearchQuery(binding.searchView.getText().toString()); }); -// NetworkArray networks = serviceAccessor.getNetworks(); -// updateNetworkCount(networks); - - // ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.networksList, networks.size() > 0); - -// ArrayList resources = new ArrayList<>(); -// for( int i = 0; i < networks.size(); i++) { -// Network network = networks.get(i); -// Status status = Status.fromString(network.getStatus()); -// resources.add(new Resource(status, network.getName(), network.getNetwork(), network.getPeer())); -// } - binding.searchView.clearFocus(); binding.searchView.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @@ -127,34 +102,9 @@ private void updateResourcesCounter(List resources) { } } -// for(int i = 0; i < resources.size(); i++) { -// Network network = networks.get(i); -// Status status = Status.fromString(network.getStatus()); -// if (status.equals(Status.CONNECTED)) { -// connected++; -// } -// } - String text = getString(R.string.resources_connected, connected, resources.size()); textPeersCount.post(() -> textPeersCount.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)) ); } - - private void updateNetworkCount(NetworkArray networks) { - TextView textPeersCount = binding.textOpenPanel; - int connected = 0; - for(int i = 0; i < networks.size(); i++) { - Network network = networks.get(i); - Status status = Status.fromString(network.getStatus()); - if (status.equals(Status.CONNECTED)) { - connected++; - } - } - - String text = getString(R.string.resources_connected, connected, networks.size()); - textPeersCount.post(() -> - textPeersCount.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)) - ); - } } From b8789f6f2922035b0fcdeaa57fd5890cd9aec948 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:39:45 -0300 Subject: [PATCH 19/34] Add support for multiple route change listeners to NetworkChangeNotifier --- .../client/tool/NetworkChangeNotifier.java | 24 ++++++++++++++---- .../io/netbird/client/tool/VPNService.java | 25 +++++++++++++------ 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java index c1708b5..f19c422 100644 --- a/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java +++ b/tool/src/main/java/io/netbird/client/tool/NetworkChangeNotifier.java @@ -5,6 +5,10 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + import io.netbird.gomobile.android.NetworkChangeListener; public class NetworkChangeNotifier implements NetworkChangeListener { @@ -13,10 +17,11 @@ public class NetworkChangeNotifier implements NetworkChangeListener { private final Context context; - private RouteChangeListener routeChangeListener; + private final List routeChangeListeners; NetworkChangeNotifier(Context context) { this.context = context; + this.routeChangeListeners = new ArrayList<>(); } @Override @@ -25,8 +30,8 @@ public void onNetworkChanged(String routes) { routes = routes.replace(",", ";"); } - if (this.routeChangeListener != null) { - this.routeChangeListener.onRouteChanged(routes); + for (var listener : routeChangeListeners) { + listener.onRouteChanged(routes); } sendBroadcast(routes); @@ -37,8 +42,17 @@ public void setInterfaceIP(String ip) { } - public void setRouteChangeListener(RouteChangeListener routeChangeListener) { - this.routeChangeListener = routeChangeListener; + public void addRouteChangeListener(RouteChangeListener routeChangeListener) { + Objects.requireNonNull(routeChangeListener); + + if (!this.routeChangeListeners.contains(routeChangeListener)) { + this.routeChangeListeners.add(routeChangeListener); + } + } + + public void removeRouteChangeListener(RouteChangeListener routeChangeListener) { + Objects.requireNonNull(routeChangeListener); + this.routeChangeListeners.remove(routeChangeListener); } private void sendBroadcast(String routes) { diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index c0b234a..2082eb3 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -8,19 +8,14 @@ import android.net.NetworkCapabilities; import android.net.VpnService; import android.os.Binder; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.os.Parcel; import android.util.Log; import androidx.annotation.Nullable; -import java.util.Random; - import io.netbird.gomobile.android.ConnectionListener; import io.netbird.gomobile.android.NetworkArray; -import io.netbird.gomobile.android.NetworkChangeListener; import io.netbird.gomobile.android.PeerInfoArray; import io.netbird.gomobile.android.URLOpener; @@ -35,6 +30,8 @@ public class VPNService extends android.net.VpnService { private TUNParameters currentTUNParameters; private NetworkChangeNotifier notifier; + private RouteChangeListener listener; + @Override public void onCreate() { super.onCreate(); @@ -45,8 +42,10 @@ public void onCreate() { var tunAdapter = new IFace(this); var iFaceDiscover = new IFaceDiscover(); + listener = this::queueTUNRenewal; + notifier = new NetworkChangeNotifier(this); - notifier.setRouteChangeListener((routes -> queueTUNRenewal(routes))); + notifier.addRouteChangeListener(listener); var preferences = new Preferences(this); var isDebuggable = Version.isDebuggable(this); @@ -97,7 +96,7 @@ public void onDestroy() { stopForeground(true); if (this.notifier != null) { - this.notifier.setRouteChangeListener(null); + this.notifier.removeRouteChangeListener(listener); } if (tunCreator != null) { @@ -167,6 +166,18 @@ public void addServiceStateListener(ServiceStateListener serviceStateListener) { public void removeServiceStateListener(ServiceStateListener serviceStateListener) { engineRunner.removeServiceStateListener(serviceStateListener); } + + public void addRouteChangeListener(RouteChangeListener listener) { + if (VPNService.this.notifier != null) { + VPNService.this.notifier.addRouteChangeListener(listener); + } + } + + public void removeRouteChangeListener(RouteChangeListener listener) { + if (VPNService.this.notifier != null) { + VPNService.this.notifier.removeRouteChangeListener(listener); + } + } } public static boolean isUsingAlwaysOnVPN(Context context) { From 3b811b4fad8ddcb6a7367420c3ae2fe8671df9d3 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:41:08 -0300 Subject: [PATCH 20/34] Add RouteChangeListener registration to VPNServiceRepository --- .../client/repository/VPNServiceRepository.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java index 98d9d9d..7804461 100644 --- a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import io.netbird.client.tool.RouteChangeListener; import io.netbird.client.tool.VPNService; import io.netbird.client.ui.home.Resource; import io.netbird.client.ui.home.Status; @@ -75,4 +76,16 @@ public List getNetworks() { return resources; } + + public void addRouteChangeListener(RouteChangeListener listener) { + if (binder != null) { + binder.addRouteChangeListener(listener); + } + } + + public void removeRouteChangeListener(RouteChangeListener listener) { + if (binder != null) { + binder.removeRouteChangeListener(listener); + } + } } From 9cb1092f39bcb8f1256d3ca39cf6c841d0b1442f Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:42:02 -0300 Subject: [PATCH 21/34] Add NetworksFragmentViewModel as a RouteChangeListener --- .../client/ui/home/NetworksFragmentViewModel.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java index 11642fb..6831cc9 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java @@ -12,8 +12,9 @@ import io.netbird.client.MyApplication; import io.netbird.client.repository.VPNServiceBindListener; import io.netbird.client.repository.VPNServiceRepository; +import io.netbird.client.tool.RouteChangeListener; -public class NetworksFragmentViewModel extends ViewModel implements VPNServiceBindListener { +public class NetworksFragmentViewModel extends ViewModel implements VPNServiceBindListener, RouteChangeListener { private final VPNServiceRepository repository; private final MutableLiveData uiState = new MutableLiveData<>(new NetworksFragmentUiState(new ArrayList<>())); @@ -27,6 +28,7 @@ public NetworksFragmentViewModel(VPNServiceRepository repository) { @Override protected void onCleared() { super.onCleared(); + repository.removeRouteChangeListener(this); repository.unbindService(); } @@ -53,4 +55,12 @@ public void getResources() { public void onBind() { getResources(); } + + @Override + public void onRouteChanged(String routes) { + var resources = repository.getNetworks(); + + // This value will be set from a background thread. + uiState.postValue(new NetworksFragmentUiState(resources)); + } } From 6a8dd37f6598614ae06400d63ba9e9dca62cde78 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 19:42:50 -0300 Subject: [PATCH 22/34] Rename VPNServiceBindListener onBind function --- .../client/repository/VPNServiceBindListener.java | 2 +- .../client/repository/VPNServiceRepository.java | 12 ++++++------ .../client/ui/home/NetworksFragmentViewModel.java | 5 +++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java b/app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java index 98f5fa3..6758514 100644 --- a/app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceBindListener.java @@ -1,5 +1,5 @@ package io.netbird.client.repository; public interface VPNServiceBindListener { - void onBind(); + void onServiceBind(); } diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java index 7804461..051a0cb 100644 --- a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java @@ -17,14 +17,14 @@ public class VPNServiceRepository { private VPNService.MyLocalBinder binder; private final Context context; - private VPNServiceBindListener listener; + private VPNServiceBindListener serviceBindListener; private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { binder = (VPNService.MyLocalBinder)service; - if (listener != null) { - listener.onBind(); + if (serviceBindListener != null) { + serviceBindListener.onServiceBind(); } } @@ -34,7 +34,7 @@ public void onServiceDisconnected(ComponentName name) { binder = null; } - listener = null; + serviceBindListener = null; } }; @@ -42,8 +42,8 @@ public VPNServiceRepository(Context context) { this.context = context; } - public void setListener(VPNServiceBindListener listener) { - this.listener = listener; + public void setServiceBindListener(VPNServiceBindListener listener) { + this.serviceBindListener = listener; } public void bindService() { diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java index 6831cc9..1dac110 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java @@ -21,7 +21,7 @@ public class NetworksFragmentViewModel extends ViewModel implements VPNServiceBi public NetworksFragmentViewModel(VPNServiceRepository repository) { this.repository = repository; - this.repository.setListener(this); + this.repository.setServiceBindListener(this); this.repository.bindService(); } @@ -52,7 +52,8 @@ public void getResources() { ); @Override - public void onBind() { + public void onServiceBind() { + this.repository.addRouteChangeListener(this); getResources(); } From d652e28d7ec426d044edeb50a1e3efd84b1b4acb Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:06:06 -0300 Subject: [PATCH 23/34] Set peer text alignment to end --- app/src/main/res/layout/list_item_resource.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/list_item_resource.xml b/app/src/main/res/layout/list_item_resource.xml index 4454355..06cf69b 100644 --- a/app/src/main/res/layout/list_item_resource.xml +++ b/app/src/main/res/layout/list_item_resource.xml @@ -64,8 +64,9 @@ tools:text="mypeer.really.long.hostname.example.com" android:textColor="@color/nb_txt_light" android:textSize="14sp" - android:maxLines="1" + android:maxLines="2" android:ellipsize="end" + android:textAlignment="textEnd" app:layout_constraintTop_toBottomOf="@id/address" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="@id/guideline_half" From 2b314d871728e5495c6ee27e6dd2540227ed7745 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Thu, 25 Sep 2025 20:30:17 -0300 Subject: [PATCH 24/34] Add switch control to list_item_resource.xml --- .../main/res/layout/list_item_resource.xml | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/app/src/main/res/layout/list_item_resource.xml b/app/src/main/res/layout/list_item_resource.xml index 06cf69b..7b70798 100644 --- a/app/src/main/res/layout/list_item_resource.xml +++ b/app/src/main/res/layout/list_item_resource.xml @@ -52,7 +52,7 @@ tools:text="172.16.254.0/24" android:textColor="@color/nb_txt_light" android:textSize="14sp" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/switch_control" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toTopOf="@id/peer" android:layout_marginEnd="16dp" /> @@ -61,19 +61,27 @@ android:id="@+id/peer" android:layout_width="0dp" android:layout_height="wrap_content" - tools:text="mypeer.really.long.hostname.example.com" - android:textColor="@color/nb_txt_light" - android:textSize="14sp" - android:maxLines="2" + android:layout_marginStart="8dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="16dp" android:ellipsize="end" + android:maxLines="1" android:textAlignment="textEnd" - app:layout_constraintTop_toBottomOf="@id/address" + android:textColor="@color/nb_txt_light" + android:textSize="14sp" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/switch_control" app:layout_constraintStart_toStartOf="@id/guideline_half" + app:layout_constraintTop_toBottomOf="@id/address" + tools:text="mypeer.really.long.hostname.example.com" /> + + + app:layout_constraintTop_toTopOf="parent" /> Date: Fri, 26 Sep 2025 21:24:59 -0300 Subject: [PATCH 25/34] Add action to select/deselect routes Adapter calls a switch toggle handler defined in NetworksFragment. Fragment, through its view model, reaches all the way to the engine to call the go client functions. --- .../repository/VPNServiceRepository.java | 15 ++++++- .../client/ui/home/NetworksAdapter.java | 25 +++++++---- .../client/ui/home/NetworksFragment.java | 19 ++++---- .../ui/home/NetworksFragmentViewModel.java | 8 ++++ .../io/netbird/client/ui/home/Resource.java | 44 +++++++++++-------- .../io/netbird/client/tool/EngineRunner.java | 20 +++++++++ .../io/netbird/client/tool/VPNService.java | 8 ++++ 7 files changed, 103 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java index 051a0cb..d4b97cc 100644 --- a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java @@ -71,7 +71,8 @@ public List getNetworks() { resources.add(new Resource(Status.fromString(network.getStatus()), network.getName(), network.getNetwork(), - network.getPeer())); + network.getPeer(), + network.getIsSelected())); } return resources; @@ -88,4 +89,16 @@ public void removeRouteChangeListener(RouteChangeListener listener) { binder.removeRouteChangeListener(listener); } } + + public void selectRoute(String route) { + if (binder != null) { + binder.selectRoute(route); + } + } + + public void deselectRoute(String route) { + if (binder != null) { + binder.deselectRoute(route); + } + } } diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java index a7f78ca..32a6650 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java @@ -1,6 +1,5 @@ package io.netbird.client.ui.home; -import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -15,17 +14,19 @@ import io.netbird.client.databinding.ListItemResourceBinding; public class NetworksAdapter extends RecyclerView.Adapter { - + public interface RouteSwitchToggleHandler { + void handleSwitchToggle(String route, boolean isChecked); + } private final List resourcesList; private final List filteredResourcesList; - - + private final RouteSwitchToggleHandler switchToggleHandler; private String filterQueryString = ""; - public NetworksAdapter(List resourcesList) { + public NetworksAdapter(List resourcesList, RouteSwitchToggleHandler switchToggleHandler) { this.resourcesList = resourcesList; filteredResourcesList = new ArrayList<>(resourcesList); + this.switchToggleHandler = switchToggleHandler; sort(); } @@ -33,8 +34,9 @@ public NetworksAdapter(List resourcesList) { @Override public ResourceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // Use ViewBinding to inflate the layout - ListItemResourceBinding binding = ListItemResourceBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); - return new ResourceViewHolder(binding); + ListItemResourceBinding binding = ListItemResourceBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false); + return new ResourceViewHolder(binding, switchToggleHandler); } @Override @@ -81,10 +83,12 @@ private void sort() { public static class ResourceViewHolder extends RecyclerView.ViewHolder { ListItemResourceBinding binding; + RouteSwitchToggleHandler switchToggleHandler; - public ResourceViewHolder(ListItemResourceBinding binding) { + public ResourceViewHolder(ListItemResourceBinding binding, RouteSwitchToggleHandler switchToggleHandler) { super(binding.getRoot()); this.binding = binding; + this.switchToggleHandler = switchToggleHandler; } public void bind(Resource resource) { @@ -92,6 +96,11 @@ public void bind(Resource resource) { binding.name.setText(resource.getName()); binding.peer.setText(resource.getPeer()); + binding.switchControl.setChecked(resource.isSelected()); + binding.switchControl.setOnCheckedChangeListener((buttonView, isChecked) -> { + this.switchToggleHandler.handleSwitchToggle(resource.getName(), isChecked); + }); + if (resource.getStatus() == Status.CONNECTED) { binding.verticalLine.setBackgroundResource(R.drawable.peer_status_connected); // Green for connected } else { diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java index 0aea246..aa24031 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java @@ -1,13 +1,10 @@ package io.netbird.client.ui.home; -import android.content.Context; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.os.Looper; import android.text.Editable; import android.text.Html; import android.text.TextWatcher; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -25,16 +22,14 @@ import java.util.List; import io.netbird.client.R; -import io.netbird.client.ServiceAccessor; import io.netbird.client.databinding.FragmentNetworksBinding; -import io.netbird.gomobile.android.Network; -import io.netbird.gomobile.android.NetworkArray; public class NetworksFragment extends Fragment { private FragmentNetworksBinding binding; private NetworksAdapter adapter; private final List resources = new ArrayList<>(); + private NetworksFragmentViewModel model; public static NetworksFragment newInstance() { return new NetworksFragment(); @@ -51,13 +46,13 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - var model = new ViewModelProvider(this, + model = new ViewModelProvider(this, ViewModelProvider.Factory.from(NetworksFragmentViewModel.initializer)) .get(NetworksFragmentViewModel.class); ZeroPeerView.setupLearnWhyClick(binding.zeroPeerLayout, requireContext()); - adapter = new NetworksAdapter(resources); + adapter = new NetworksAdapter(resources, this::routeSwitchToggleHandler); RecyclerView resourcesRecyclerView = binding.networksRecyclerView; resourcesRecyclerView.setAdapter(adapter); @@ -107,4 +102,12 @@ private void updateResourcesCounter(List resources) { textPeersCount.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)) ); } + + private void routeSwitchToggleHandler(String route, boolean isChecked) { + if (isChecked) { + model.selectRoute(route); + } else { + model.deselectRoute(route); + } + } } diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java index 1dac110..73606e4 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java @@ -64,4 +64,12 @@ public void onRouteChanged(String routes) { // This value will be set from a background thread. uiState.postValue(new NetworksFragmentUiState(resources)); } + + public void selectRoute(String route) { + this.repository.selectRoute(route); + } + + public void deselectRoute(String route) { + this.repository.deselectRoute(route); + } } diff --git a/app/src/main/java/io/netbird/client/ui/home/Resource.java b/app/src/main/java/io/netbird/client/ui/home/Resource.java index fe83f11..5fec31d 100644 --- a/app/src/main/java/io/netbird/client/ui/home/Resource.java +++ b/app/src/main/java/io/netbird/client/ui/home/Resource.java @@ -1,35 +1,41 @@ package io.netbird.client.ui.home; public class Resource { - private final Status status; - private final String name; - private final String address; - private final String peer; - - public Resource(Status status, String name, String address, String peer) { - this.status = status; - this.name = name; - this.address = address; - this.peer = peer; - } + private final Status status; + private final String name; + private final String address; + private final String peer; + private final boolean isSelected; + public Resource(Status status, String name, String address, String peer, boolean isSelected) { + this.status = status; + this.name = name; + this.address = address; + this.peer = peer; + this.isSelected = isSelected; + } - public String getName() { - return name; - } + public String getName() { + return name; + } - public String getAddress() { - return address; - } + public String getAddress() { + return address; + } public String getPeer() { return peer; } public Status getStatus() { - return status; + return status; } + public boolean isExitNode() { - return address.equals("0.0.0.0/0"); + return address.equals("0.0.0.0/0"); + } + + public boolean isSelected() { + return isSelected; } } \ No newline at end of file diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index 7a512a6..58929d5 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -164,4 +164,24 @@ public void renewTUN(int fd) { notifyError(e); } } + + public void selectRoute(String route) { + Log.d(LOGTAG, String.format("selecting route: %s", route)); + try { + goClient.selectRoute(route); + } catch (Exception e) { + Log.e(LOGTAG, "goClient error", e); + notifyError(e); + } + } + + public void deselectRoute(String route) { + Log.d(LOGTAG, String.format("deselecting route: %s", route)); + try { + goClient.deselectRoute(route); + } catch (Exception e) { + Log.e(LOGTAG, "goClient error", e); + notifyError(e); + } + } } diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index 2082eb3..20f6684 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -178,6 +178,14 @@ public void removeRouteChangeListener(RouteChangeListener listener) { VPNService.this.notifier.removeRouteChangeListener(listener); } } + + public void selectRoute(String route) { + engineRunner.selectRoute(route); + } + + public void deselectRoute(String route) { + engineRunner.deselectRoute(route); + } } public static boolean isUsingAlwaysOnVPN(Context context) { From 3ffafe15515ad9e2876701db4ddbfa9e9fcb4a43 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Fri, 26 Sep 2025 21:26:41 -0300 Subject: [PATCH 26/34] Add stateFile method to Preferences to retrieve a location with read/write access on android's context --- tool/src/main/java/io/netbird/client/tool/Preferences.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tool/src/main/java/io/netbird/client/tool/Preferences.java b/tool/src/main/java/io/netbird/client/tool/Preferences.java index 1f9c7b7..c580749 100644 --- a/tool/src/main/java/io/netbird/client/tool/Preferences.java +++ b/tool/src/main/java/io/netbird/client/tool/Preferences.java @@ -14,6 +14,9 @@ public class Preferences { public static String configFile(Context context){ return context.getFilesDir().getPath() + "/netbird.cfg"; } + public static String stateFile(Context context) { + return context.getFilesDir().getPath() + "/state.json"; + } public Preferences(Context context) { sharedPref = context.getSharedPreferences("netbird", Context.MODE_PRIVATE); From b7d62c0978f2dfe0b6b37b7b54f326f71c896086 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Fri, 26 Sep 2025 21:27:42 -0300 Subject: [PATCH 27/34] Send state file path to engine's go client --- .../src/main/java/io/netbird/client/tool/EngineRunner.java | 7 +++++-- tool/src/main/java/io/netbird/client/tool/VPNService.java | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index 58929d5..e8ae15c 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -26,7 +26,9 @@ class EngineRunner { Set serviceStateListeners = new HashSet<>(); private final Client goClient; - public EngineRunner(String configurationFilePath, NetworkChangeListener networkChangeListener, TunAdapter tunAdapter, IFaceDiscover iFaceDiscover, String versionName, boolean isTraceLogEnabled, boolean isDebuggable) { + public EngineRunner(String configurationFilePath, NetworkChangeListener networkChangeListener, TunAdapter tunAdapter, + IFaceDiscover iFaceDiscover, String versionName, boolean isTraceLogEnabled, boolean isDebuggable, + String stateFilePath) { goClient = Android.newClient( configurationFilePath, androidSDKVersion(), @@ -34,7 +36,8 @@ public EngineRunner(String configurationFilePath, NetworkChangeListener networkC versionName, tunAdapter, iFaceDiscover, - networkChangeListener); + networkChangeListener, + stateFilePath); updateLogLevel(isTraceLogEnabled, isDebuggable); } diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index 20f6684..5f77d29 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -38,6 +38,7 @@ public void onCreate() { Log.d(LOGTAG, "onCreate"); var configurationFilePath = Preferences.configFile(this); + var stateFilePath = Preferences.stateFile(this); var versionName = Version.getVersionName(this); var tunAdapter = new IFace(this); var iFaceDiscover = new IFaceDiscover(); @@ -51,7 +52,7 @@ public void onCreate() { var isDebuggable = Version.isDebuggable(this); engineRunner = new EngineRunner(configurationFilePath, notifier, tunAdapter, iFaceDiscover, versionName, - preferences.isTraceLogEnabled(), isDebuggable); + preferences.isTraceLogEnabled(), isDebuggable, stateFilePath); fgNotification = new ForegroundNotification(this); engineRunner.addServiceStateListener(serviceStateListener); } From d871f94d7e7fcc812833af66c2244b5fd2e0561a Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Fri, 26 Sep 2025 21:48:10 -0300 Subject: [PATCH 28/34] Set submodule reference to forked repo --- .gitmodules | 3 ++- netbird | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 932099a..b5c5152 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,4 @@ [submodule "netbird"] path = netbird - url = https://github.com/netbirdio/netbird.git + url = https://github.com/doromaraujo/netbird.git + branch = feature/android-allow-selecting-routes \ No newline at end of file diff --git a/netbird b/netbird index b2fbdfa..53d706a 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit b2fbdfabeecf9c76af84847ba3e36492317799bc +Subproject commit 53d706ad8e4b137c5bc0905b4b251dd5c3f764d9 From 192f6fec40ecb07b8bca005e59b67d30cb3d4d0b Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Tue, 30 Sep 2025 20:51:19 -0300 Subject: [PATCH 29/34] Use routing peers on networks adapter to display status color --- .../repository/VPNServiceRepository.java | 58 ++++++++++++++++++- .../client/ui/home/NetworksAdapter.java | 29 +++++++--- .../client/ui/home/NetworksFragment.java | 6 +- .../ui/home/NetworksFragmentUiState.java | 7 ++- .../ui/home/NetworksFragmentViewModel.java | 8 ++- .../io/netbird/client/ui/home/Resource.java | 10 +++- .../netbird/client/ui/home/RoutingPeer.java | 20 +++++++ .../res/drawable/peer_status_selected.xml | 5 ++ netbird | 2 +- 9 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/io/netbird/client/ui/home/RoutingPeer.java create mode 100644 app/src/main/res/drawable/peer_status_selected.xml diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java index d4b97cc..5268e39 100644 --- a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java @@ -12,7 +12,10 @@ import io.netbird.client.tool.RouteChangeListener; import io.netbird.client.tool.VPNService; import io.netbird.client.ui.home.Resource; +import io.netbird.client.ui.home.RoutingPeer; import io.netbird.client.ui.home.Status; +import io.netbird.gomobile.android.NetworkDomains; +import io.netbird.gomobile.android.PeerRoutes; public class VPNServiceRepository { private VPNService.MyLocalBinder binder; @@ -22,7 +25,7 @@ public class VPNServiceRepository { private final ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { - binder = (VPNService.MyLocalBinder)service; + binder = (VPNService.MyLocalBinder) service; if (serviceBindListener != null) { serviceBindListener.onServiceBind(); } @@ -42,6 +45,34 @@ public VPNServiceRepository(Context context) { this.context = context; } + private List createPeerRoutesList(PeerRoutes peerRoutes) { + List routes = new ArrayList<>(); + + try { + for (int i = 0; i < peerRoutes.size(); i++) { + routes.add(peerRoutes.get(i)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + return routes; + } + + private List createNetworkDomainsList(NetworkDomains networkDomains) { + List domains = new ArrayList<>(); + + try { + for (int i = 0; i < networkDomains.size(); i++) { + domains.add(networkDomains.get(i)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + + return domains; + } + public void setServiceBindListener(VPNServiceBindListener listener) { this.serviceBindListener = listener; } @@ -68,16 +99,39 @@ public List getNetworks() { for (int i = 0; i < networks.size(); i++) { var network = networks.get(i); + var networkDomains = network.getNetworkDomains(); + resources.add(new Resource(Status.fromString(network.getStatus()), network.getName(), network.getNetwork(), network.getPeer(), - network.getIsSelected())); + network.getIsSelected(), + createNetworkDomainsList(networkDomains))); } return resources; } + public List getRoutingPeers() { + if (binder == null) { + return new ArrayList<>(); + } + + var peers = new ArrayList(); + var peersFromEngine = binder.peersInfo(); + + for (int i = 0; i < peersFromEngine.size(); i++) { + var peerInfo = peersFromEngine.get(i); + var peerRoutes = peerInfo.getPeerRoutes(); + + peers.add(new RoutingPeer( + Status.fromString(peerInfo.getConnStatus()), + createPeerRoutesList(peerRoutes))); + } + + return peers; + } + public void addRouteChangeListener(RouteChangeListener listener) { if (binder != null) { binder.addRouteChangeListener(listener); diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java index 32a6650..cb7d538 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java @@ -3,10 +3,10 @@ import android.view.LayoutInflater; import android.view.ViewGroup; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; - import java.util.ArrayList; import java.util.List; @@ -19,12 +19,14 @@ public interface RouteSwitchToggleHandler { } private final List resourcesList; + private final List peers; private final List filteredResourcesList; private final RouteSwitchToggleHandler switchToggleHandler; private String filterQueryString = ""; - public NetworksAdapter(List resourcesList, RouteSwitchToggleHandler switchToggleHandler) { + public NetworksAdapter(List resourcesList, List peers, RouteSwitchToggleHandler switchToggleHandler) { this.resourcesList = resourcesList; + this.peers = peers; filteredResourcesList = new ArrayList<>(resourcesList); this.switchToggleHandler = switchToggleHandler; sort(); @@ -41,7 +43,7 @@ public ResourceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int view @Override public void onBindViewHolder(@NonNull ResourceViewHolder holder, int position) { - holder.bind(filteredResourcesList.get(position)); + holder.bind(filteredResourcesList.get(position), peers); } @Override @@ -91,7 +93,20 @@ public ResourceViewHolder(ListItemResourceBinding binding, RouteSwitchToggleHand this.switchToggleHandler = switchToggleHandler; } - public void bind(Resource resource) { + @DrawableRes + private int getConnectionStatusIndicatorDrawable(Resource resource, List peers) { + var connectedPeers = peers.stream() + .filter(peer -> peer.getStatus().equals(Status.CONNECTED)) + .filter(peer -> peer.getRoutes().contains(resource.getAddress())) + .count(); + + if (connectedPeers > 0) return R.drawable.peer_status_connected; + + if (resource.isSelected()) return R.drawable.peer_status_selected; + return R.drawable.peer_status_disconnected; + } + + public void bind(Resource resource, List peers) { binding.address.setText(resource.getAddress()); binding.name.setText(resource.getName()); binding.peer.setText(resource.getPeer()); @@ -101,11 +116,7 @@ public void bind(Resource resource) { this.switchToggleHandler.handleSwitchToggle(resource.getName(), isChecked); }); - if (resource.getStatus() == Status.CONNECTED) { - binding.verticalLine.setBackgroundResource(R.drawable.peer_status_connected); // Green for connected - } else { - binding.verticalLine.setBackgroundResource(R.drawable.peer_status_disconnected); // Red for disconnected - } + binding.verticalLine.setBackgroundResource(getConnectionStatusIndicatorDrawable(resource, peers)); if(resource.isExitNode()) { binding.exitNode.setVisibility(android.view.View.VISIBLE); diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java index aa24031..eedb358 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java @@ -29,6 +29,7 @@ public class NetworksFragment extends Fragment { private FragmentNetworksBinding binding; private NetworksAdapter adapter; private final List resources = new ArrayList<>(); + private final List peers = new ArrayList<>(); private NetworksFragmentViewModel model; public static NetworksFragment newInstance() { @@ -52,7 +53,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat ZeroPeerView.setupLearnWhyClick(binding.zeroPeerLayout, requireContext()); - adapter = new NetworksAdapter(resources, this::routeSwitchToggleHandler); + adapter = new NetworksAdapter(resources, peers, this::routeSwitchToggleHandler); RecyclerView resourcesRecyclerView = binding.networksRecyclerView; resourcesRecyclerView.setAdapter(adapter); @@ -62,6 +63,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat resources.clear(); resources.addAll(uiState.getResources()); + peers.clear(); + peers.addAll(uiState.getPeers()); + updateResourcesCounter(resources); ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.networksList, !resources.isEmpty()); adapter.notifyDataSetChanged(); diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentUiState.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentUiState.java index bef16df..bede7a5 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentUiState.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentUiState.java @@ -4,12 +4,17 @@ public class NetworksFragmentUiState { private final List resources; + private final List peers; - public NetworksFragmentUiState(List resources) { + public NetworksFragmentUiState(List resources, List peers) { this.resources = resources; + this.peers = peers; } public List getResources() { return resources; } + + public List getPeers() { return peers; + } } diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java index 73606e4..921b953 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java @@ -17,7 +17,7 @@ public class NetworksFragmentViewModel extends ViewModel implements VPNServiceBindListener, RouteChangeListener { private final VPNServiceRepository repository; private final MutableLiveData uiState = - new MutableLiveData<>(new NetworksFragmentUiState(new ArrayList<>())); + new MutableLiveData<>(new NetworksFragmentUiState(new ArrayList<>(), new ArrayList<>())); public NetworksFragmentViewModel(VPNServiceRepository repository) { this.repository = repository; @@ -38,8 +38,9 @@ public LiveData getUiState() { public void getResources() { var resources = repository.getNetworks(); + var peers = repository.getRoutingPeers(); - uiState.setValue(new NetworksFragmentUiState(resources)); + uiState.setValue(new NetworksFragmentUiState(resources, peers)); } static final ViewModelInitializer initializer = new ViewModelInitializer<>( @@ -60,9 +61,10 @@ public void onServiceBind() { @Override public void onRouteChanged(String routes) { var resources = repository.getNetworks(); + var peers = repository.getRoutingPeers(); // This value will be set from a background thread. - uiState.postValue(new NetworksFragmentUiState(resources)); + uiState.postValue(new NetworksFragmentUiState(resources, peers)); } public void selectRoute(String route) { diff --git a/app/src/main/java/io/netbird/client/ui/home/Resource.java b/app/src/main/java/io/netbird/client/ui/home/Resource.java index 5fec31d..19675f0 100644 --- a/app/src/main/java/io/netbird/client/ui/home/Resource.java +++ b/app/src/main/java/io/netbird/client/ui/home/Resource.java @@ -1,18 +1,22 @@ package io.netbird.client.ui.home; +import java.util.List; + public class Resource { private final Status status; private final String name; private final String address; private final String peer; private final boolean isSelected; + private final List domains; - public Resource(Status status, String name, String address, String peer, boolean isSelected) { + public Resource(Status status, String name, String address, String peer, boolean isSelected, List domains) { this.status = status; this.name = name; this.address = address; this.peer = peer; this.isSelected = isSelected; + this.domains = domains; } public String getName() { @@ -38,4 +42,8 @@ public boolean isExitNode() { public boolean isSelected() { return isSelected; } + + public List getDomains() { + return this.domains; + } } \ No newline at end of file diff --git a/app/src/main/java/io/netbird/client/ui/home/RoutingPeer.java b/app/src/main/java/io/netbird/client/ui/home/RoutingPeer.java new file mode 100644 index 0000000..e5f63fc --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/home/RoutingPeer.java @@ -0,0 +1,20 @@ +package io.netbird.client.ui.home; + +import java.util.List; + +public class RoutingPeer { + private final Status status; + private final List routes; + public RoutingPeer(Status status, List routes) { + this.status = status; + this.routes = routes; + } + + public Status getStatus() { + return this.status; + } + + public List getRoutes() { + return this.routes; + } +} diff --git a/app/src/main/res/drawable/peer_status_selected.xml b/app/src/main/res/drawable/peer_status_selected.xml new file mode 100644 index 0000000..0483122 --- /dev/null +++ b/app/src/main/res/drawable/peer_status_selected.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/netbird b/netbird index 53d706a..41fc5aa 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit 53d706ad8e4b137c5bc0905b4b251dd5c3f764d9 +Subproject commit 41fc5aaacee9b0b57ddc94fdec871c04c6531ba4 From 1fc190190e4e97790cc1497a2ed3823b4397bbfe Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:33:33 -0300 Subject: [PATCH 30/34] Use isSelected from resource to count it as a connected one --- .../main/java/io/netbird/client/ui/home/NetworksFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java index eedb358..d47f9f4 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java @@ -96,7 +96,7 @@ private void updateResourcesCounter(List resources) { int connected = 0; for (var resource : resources) { - if (resource.getStatus().equals(Status.CONNECTED)) { + if (resource.isSelected()) { connected++; } } From 6a726f254a0474057748677e3cf9073bdfc15d7f Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:34:47 -0300 Subject: [PATCH 31/34] Add NetworkDomain to Resource Network domain stores both its address and a list of resolved ip addresses --- .../repository/VPNServiceRepository.java | 21 ++++++++++++--- .../netbird/client/ui/home/NetworkDomain.java | 26 +++++++++++++++++++ .../io/netbird/client/ui/home/Resource.java | 6 ++--- 3 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/io/netbird/client/ui/home/NetworkDomain.java diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java index 5268e39..5953d3f 100644 --- a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java @@ -11,6 +11,7 @@ import io.netbird.client.tool.RouteChangeListener; import io.netbird.client.tool.VPNService; +import io.netbird.client.ui.home.NetworkDomain; import io.netbird.client.ui.home.Resource; import io.netbird.client.ui.home.RoutingPeer; import io.netbird.client.ui.home.Status; @@ -59,12 +60,26 @@ private List createPeerRoutesList(PeerRoutes peerRoutes) { return routes; } - private List createNetworkDomainsList(NetworkDomains networkDomains) { - List domains = new ArrayList<>(); + private List createNetworkDomainsList(NetworkDomains networkDomains) { + List domains = new ArrayList<>(); + + io.netbird.gomobile.android.NetworkDomain goNetworkDomain; + NetworkDomain networkDomain; + String ipAddress; try { for (int i = 0; i < networkDomains.size(); i++) { - domains.add(networkDomains.get(i)); + goNetworkDomain = networkDomains.get(i); + networkDomain = new NetworkDomain(goNetworkDomain.getAddress()); + + var resolvedIPs = goNetworkDomain.getResolvedIPs(); + + for (int j = 0; j < resolvedIPs.size(); j++) { + ipAddress = resolvedIPs.get(j); + networkDomain.addResolvedIP(ipAddress); + } + + domains.add(networkDomain); } } catch (Exception e) { throw new RuntimeException(e); diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworkDomain.java b/app/src/main/java/io/netbird/client/ui/home/NetworkDomain.java new file mode 100644 index 0000000..01a375e --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/home/NetworkDomain.java @@ -0,0 +1,26 @@ +package io.netbird.client.ui.home; + +import java.util.ArrayList; +import java.util.List; + +public class NetworkDomain { + private final String address; + private final List resolvedIPs; + + public NetworkDomain(String address) { + this.address = address; + this.resolvedIPs = new ArrayList<>(); + } + + public String getAddress() { + return address; + } + + public void addResolvedIP(String ipAddress) { + this.resolvedIPs.add(ipAddress); + } + + public List getResolvedIPs() { + return this.resolvedIPs; + } +} diff --git a/app/src/main/java/io/netbird/client/ui/home/Resource.java b/app/src/main/java/io/netbird/client/ui/home/Resource.java index 19675f0..813904f 100644 --- a/app/src/main/java/io/netbird/client/ui/home/Resource.java +++ b/app/src/main/java/io/netbird/client/ui/home/Resource.java @@ -8,9 +8,9 @@ public class Resource { private final String address; private final String peer; private final boolean isSelected; - private final List domains; + private final List domains; - public Resource(Status status, String name, String address, String peer, boolean isSelected, List domains) { + public Resource(Status status, String name, String address, String peer, boolean isSelected, List domains) { this.status = status; this.name = name; this.address = address; @@ -43,7 +43,7 @@ public boolean isSelected() { return isSelected; } - public List getDomains() { + public List getDomains() { return this.domains; } } \ No newline at end of file From ca1b87b3028fa2efcd9e915066dad0f787703333 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Wed, 1 Oct 2025 14:36:03 -0300 Subject: [PATCH 32/34] Add extra clause to getConnectionStatusIndicatorDrawable It now also takes into consideration resources mapped to domains and its resolved IP addresses in order to check connectivity --- .../client/ui/home/NetworksAdapter.java | 39 +++++++++++++++++-- netbird | 2 +- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java index cb7d538..2211813 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import io.netbird.client.R; import io.netbird.client.databinding.ListItemResourceBinding; @@ -71,7 +72,7 @@ private void doFilterBySearchQuery() { ArrayList temporaryList = new ArrayList<>(resourcesList); for (Resource res : temporaryList) { - if (res.getName().toLowerCase().contains(filterQueryString.toLowerCase())){ + if (res.getName().toLowerCase().contains(filterQueryString.toLowerCase())) { filteredResourcesList.add(res); } } @@ -93,14 +94,46 @@ public ResourceViewHolder(ListItemResourceBinding binding, RouteSwitchToggleHand this.switchToggleHandler = switchToggleHandler; } + /** + *

+ * Returns a drawable indicating whether a given resource is CONNECTED, SELECTED or DESELECTED. + * A resource is considered CONNECTED when, given a list of routing peers, at least one of them + * also has a CONNECTED status and contains a route that maps to that given resource's address + *

+ *

+ * OR + *

+ *

+ * if the resource is mapped to a domain whose any of its resolved IP addresses is contained + * in any of the CONNECTED routing peer's routes. + *

+ * Barring those conditions, it simply checks if the resource is selected or not. + *

+ */ @DrawableRes private int getConnectionStatusIndicatorDrawable(Resource resource, List peers) { var connectedPeers = peers.stream() .filter(peer -> peer.getStatus().equals(Status.CONNECTED)) + .collect(Collectors.toList()); + + var totalPeersWithRouteMatchingResourceAddress = connectedPeers.stream() .filter(peer -> peer.getRoutes().contains(resource.getAddress())) .count(); - if (connectedPeers > 0) return R.drawable.peer_status_connected; + if (totalPeersWithRouteMatchingResourceAddress > 0) { + return R.drawable.peer_status_connected; + } + + var allResolvedIPAddresses = resource.getDomains().stream() + .flatMap(domain -> domain.getResolvedIPs().stream()); + + var allConnectedRoutingPeerRoutes = connectedPeers.stream() + .flatMap(peer -> peer.getRoutes().stream()) + .collect(Collectors.toList()); + + if (allResolvedIPAddresses.anyMatch(allConnectedRoutingPeerRoutes::contains)) { + return R.drawable.peer_status_connected; + } if (resource.isSelected()) return R.drawable.peer_status_selected; return R.drawable.peer_status_disconnected; @@ -118,7 +151,7 @@ public void bind(Resource resource, List peers) { binding.verticalLine.setBackgroundResource(getConnectionStatusIndicatorDrawable(resource, peers)); - if(resource.isExitNode()) { + if (resource.isExitNode()) { binding.exitNode.setVisibility(android.view.View.VISIBLE); } else { binding.exitNode.setVisibility(android.view.View.GONE); diff --git a/netbird b/netbird index 41fc5aa..970e3de 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit 41fc5aaacee9b0b57ddc94fdec871c04c6531ba4 +Subproject commit 970e3dea8b690963954a6cca23fda77e50c37e44 From 3d2cefb44996cc80c88fa33cc99e07628b6b66be Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:20:17 -0300 Subject: [PATCH 33/34] Propagate exception when selecting / deselecting routes So that when toggling a route switch, it won't be in an inconsistent state in case of error from engine --- .../client/repository/VPNServiceRepository.java | 4 ++-- .../netbird/client/ui/home/NetworksAdapter.java | 15 +++++++++++++-- .../netbird/client/ui/home/NetworksFragment.java | 2 +- .../client/ui/home/NetworksFragmentViewModel.java | 4 ++-- .../java/io/netbird/client/tool/EngineRunner.java | 6 ++++-- .../java/io/netbird/client/tool/VPNService.java | 4 ++-- 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java index 5953d3f..990ba7c 100644 --- a/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java +++ b/app/src/main/java/io/netbird/client/repository/VPNServiceRepository.java @@ -159,13 +159,13 @@ public void removeRouteChangeListener(RouteChangeListener listener) { } } - public void selectRoute(String route) { + public void selectRoute(String route) throws Exception { if (binder != null) { binder.selectRoute(route); } } - public void deselectRoute(String route) { + public void deselectRoute(String route) throws Exception { if (binder != null) { binder.deselectRoute(route); } diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java index 2211813..e25a6eb 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java @@ -16,7 +16,7 @@ public class NetworksAdapter extends RecyclerView.Adapter { public interface RouteSwitchToggleHandler { - void handleSwitchToggle(String route, boolean isChecked); + void handleSwitchToggle(String route, boolean isChecked) throws Exception; } private final List resourcesList; @@ -145,8 +145,19 @@ public void bind(Resource resource, List peers) { binding.peer.setText(resource.getPeer()); binding.switchControl.setChecked(resource.isSelected()); + binding.switchControl.setTag(false); binding.switchControl.setOnCheckedChangeListener((buttonView, isChecked) -> { - this.switchToggleHandler.handleSwitchToggle(resource.getName(), isChecked); + try { + boolean tag = (boolean)binding.switchControl.getTag(); + if (!tag) { + this.switchToggleHandler.handleSwitchToggle(resource.getName(), isChecked); + } + } catch (Exception ignored) { + // This is done so that reversing the toggle action won't retrigger the toggle handler. + binding.switchControl.setTag(true); + binding.switchControl.setChecked(!isChecked); + binding.switchControl.setTag(false); + } }); binding.verticalLine.setBackgroundResource(getConnectionStatusIndicatorDrawable(resource, peers)); diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java index d47f9f4..46260af 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragment.java @@ -107,7 +107,7 @@ private void updateResourcesCounter(List resources) { ); } - private void routeSwitchToggleHandler(String route, boolean isChecked) { + private void routeSwitchToggleHandler(String route, boolean isChecked) throws Exception { if (isChecked) { model.selectRoute(route); } else { diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java index 921b953..db5b79d 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksFragmentViewModel.java @@ -67,11 +67,11 @@ public void onRouteChanged(String routes) { uiState.postValue(new NetworksFragmentUiState(resources, peers)); } - public void selectRoute(String route) { + public void selectRoute(String route) throws Exception { this.repository.selectRoute(route); } - public void deselectRoute(String route) { + public void deselectRoute(String route) throws Exception { this.repository.deselectRoute(route); } } diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java index e8ae15c..124ffee 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -168,23 +168,25 @@ public void renewTUN(int fd) { } } - public void selectRoute(String route) { + public void selectRoute(String route) throws Exception { Log.d(LOGTAG, String.format("selecting route: %s", route)); try { goClient.selectRoute(route); } catch (Exception e) { Log.e(LOGTAG, "goClient error", e); notifyError(e); + throw e; } } - public void deselectRoute(String route) { + public void deselectRoute(String route) throws Exception { Log.d(LOGTAG, String.format("deselecting route: %s", route)); try { goClient.deselectRoute(route); } catch (Exception e) { Log.e(LOGTAG, "goClient error", e); notifyError(e); + throw e; } } } diff --git a/tool/src/main/java/io/netbird/client/tool/VPNService.java b/tool/src/main/java/io/netbird/client/tool/VPNService.java index 5f77d29..1639d8a 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -180,11 +180,11 @@ public void removeRouteChangeListener(RouteChangeListener listener) { } } - public void selectRoute(String route) { + public void selectRoute(String route) throws Exception { engineRunner.selectRoute(route); } - public void deselectRoute(String route) { + public void deselectRoute(String route) throws Exception { engineRunner.deselectRoute(route); } } From 5675522ae0b70d2731aa7e303d3719aa8d18ede6 Mon Sep 17 00:00:00 2001 From: Diego Romar <18450339+doromaraujo@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:32:12 -0300 Subject: [PATCH 34/34] Set switch control tag to true when setting its checked value --- .../main/java/io/netbird/client/ui/home/NetworksAdapter.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java index e25a6eb..19a8110 100644 --- a/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java +++ b/app/src/main/java/io/netbird/client/ui/home/NetworksAdapter.java @@ -144,6 +144,9 @@ public void bind(Resource resource, List peers) { binding.name.setText(resource.getName()); binding.peer.setText(resource.getPeer()); + // Necessary when rebinding because onCheckedChangeListener is already set. + binding.switchControl.setTag(true); + binding.switchControl.setChecked(resource.isSelected()); binding.switchControl.setTag(false); binding.switchControl.setOnCheckedChangeListener((buttonView, isChecked) -> {