Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6eb07a3
Fix UI stuck on Disconnected during network-change engine restart
pappz Apr 20, 2026
f0df3f5
Bind process to default network and ignore initial callback burst
pappz Apr 20, 2026
b52ce5d
Bump netbird submodule to test branch
pappz Apr 20, 2026
212cf42
Gate network change notifications on engine running
pappz Apr 20, 2026
5a96d64
Merge branch 'main' into fix/reconnection-notification
pappz Apr 20, 2026
bd31953
Update submodule
pappz Apr 20, 2026
b2d0f6d
Silence foreground service notification
pappz Apr 20, 2026
ff71758
Guard default network callback against stale events
pappz Apr 24, 2026
5c6391e
Merge remote-tracking branch 'origin/main' into fix/reconnection-noti…
pappz Apr 24, 2026
48ae2d5
Serialize default network callback state changes
pappz Apr 27, 2026
86b12a0
Warn if default network is a VPN
pappz Apr 27, 2026
2e95a1c
Merge branch 'main' into fix/reconnection-notification
pappz Apr 27, 2026
3bd06f1
Serialize default network callback registration
pappz Apr 27, 2026
c28481f
Suppress old engine state events during restart
pappz Apr 27, 2026
0d8a086
Detect network handover from default-network signal
pappz Apr 27, 2026
061b516
Skip restart when engine reconnects on its own
pappz Apr 27, 2026
878e8b9
Bump netbird submodule to fix/job-stream-state-leak
pappz Apr 27, 2026
380808a
Skip bindProcessToNetwork when default network is a VPN
pappz Apr 27, 2026
4bdbf71
Skip bindProcessToNetwork when network capabilities are unknown
pappz Apr 27, 2026
697fe80
Cancel pending restart on user-driven engine actions
pappz Apr 27, 2026
fb3a440
Remove bindProcessToNetwork from default network callback
pappz Apr 27, 2026
c257fb5
Fix wrapper stacking and stale listener on restart timeout
pappz Apr 28, 2026
d202068
Address CodeRabbit review nits and bump submodule
pappz Apr 29, 2026
8c86028
Merge remote-tracking branch 'origin/main' into fix/reconnection-noti…
pappz Apr 29, 2026
11baba2
Merge branch 'main' into fix/reconnection-notification
pappz Apr 30, 2026
c43d4da
Move peer-list refreshes off the UI thread
pappz Apr 30, 2026
124c8a5
Fix races and listener-suppression leak in EngineRestarter
pappz May 5, 2026
5bbcdf5
Guard PeersFragmentViewModel against teardown race
pappz May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion netbird
Submodule netbird updated 132 files
29 changes: 29 additions & 0 deletions tool/src/main/java/io/netbird/client/tool/EngineRestarter.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.util.Log;

import io.netbird.client.tool.networks.NetworkToggleListener;
import io.netbird.gomobile.android.ConnectionListener;

/**
* <p>EngineRestarter restarts the Go engine.</p>
Expand Down Expand Up @@ -52,6 +53,7 @@ private void restartEngine() {
if (isRestartInProgress) {
Log.e(LOGTAG, "engine restart timeout - forcing flag reset");
isRestartInProgress = false;
notifyDisconnected();
}
Comment thread
pappz marked this conversation as resolved.
};

Expand All @@ -72,6 +74,7 @@ public void onStarted() {
@Override
public void onStopped() {
Log.d(LOGTAG, "engine is stopped, restarting...");
notifyConnecting();
engineRunner.runWithoutAuth();
}

Expand All @@ -81,6 +84,7 @@ public void onError(String msg) {
isRestartInProgress = false; // Resetting flag on error as well
handler.removeCallbacks(timeoutCallback); // Cancel timeout
engineRunner.removeServiceStateListener(this);
notifyDisconnected();
}
};
currentListener = serviceStateListener;
Expand All @@ -94,9 +98,34 @@ public void onError(String msg) {
}

Log.d(LOGTAG, "engine is running, stopping due to network change");
notifyConnecting();
engineRunner.stop();
}

private void notifyConnecting() {
ConnectionListener listener = engineRunner.getConnectionListener();
if (listener == null) {
return;
}
try {
listener.onConnecting();
} catch (Exception e) {
Log.w(LOGTAG, "onConnecting notification failed: " + e.getMessage());
}
}

private void notifyDisconnected() {
ConnectionListener listener = engineRunner.getConnectionListener();
if (listener == null) {
return;
}
try {
listener.onDisconnected();
} catch (Exception e) {
Log.w(LOGTAG, "onDisconnected notification failed: " + e.getMessage());
}
}

@Override
public void onNetworkTypeChanged() {
Log.d(LOGTAG, "network type changed, scheduling restart with "
Expand Down
7 changes: 7 additions & 0 deletions tool/src/main/java/io/netbird/client/tool/EngineRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class EngineRunner {
private boolean engineIsRunning = false;
Set<ServiceStateListener> serviceStateListeners = ConcurrentHashMap.newKeySet();
private final Client goClient;
private ConnectionListener connectionListener;

public EngineRunner(Context context, NetworkChangeListener networkChangeListener, TunAdapter tunAdapter,
IFaceDiscover iFaceDiscover, String versionName, boolean isTraceLogEnabled, boolean isDebuggable,
Expand Down Expand Up @@ -124,13 +125,19 @@ public synchronized boolean isRunning() {
}

public synchronized void setConnectionListener(ConnectionListener listener) {
this.connectionListener = listener;
goClient.setConnectionListener(listener);
}

public synchronized void removeStatusListener() {
this.connectionListener = null;
goClient.removeConnectionListener();
}

synchronized ConnectionListener getConnectionListener() {
return connectionListener;
}

public synchronized void addServiceStateListener(ServiceStateListener serviceStateListener) {
if (engineIsRunning) {
serviceStateListener.onStarted();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ public void startForeground() {
NotificationChannel channel = new NotificationChannel(
channelId,
service.getResources().getString(R.string.fg_notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager.IMPORTANCE_LOW);
channel.setSound(null, null);
channel.enableVibration(false);
((NotificationManager) service.getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);

Intent notificationIntent = new Intent();
Expand Down
9 changes: 5 additions & 4 deletions tool/src/main/java/io/netbird/client/tool/VPNService.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,15 @@ public void onCreate() {
// Create foreground notification before initializing engine
fgNotification = new ForegroundNotification(this);

// Create network availability listener before initializing engine
networkAvailabilityListener = new ConcreteNetworkAvailabilityListener();


engineRunner = new EngineRunner(this, notifier, tunAdapter, iFaceDiscover, versionName,
preferences.isTraceLogEnabled(), Version.isDebuggable(this), profileManager);
engineRunner.addServiceStateListener(serviceStateListener);

// Create network availability listener after the engine runner so we
// can gate notifications on the engine actually being up; this avoids
// acting on Android's initial onAvailable burst during cold start.
networkAvailabilityListener = new ConcreteNetworkAvailabilityListener(engineRunner::isRunning);

engineRestarter = new EngineRestarter(engineRunner);
networkAvailabilityListener.subscribe(engineRestarter);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BooleanSupplier;

public class ConcreteNetworkAvailabilityListener implements NetworkAvailabilityListener {
private final Map<Integer, Boolean> availableNetworkTypes;
private final BooleanSupplier shouldNotify;
private NetworkToggleListener listener;

public ConcreteNetworkAvailabilityListener() {
this(() -> true);
}

// shouldNotify is consulted before each listener notification. Pass
// engineRunner::isRunning to swallow the initial onAvailable burst that
// fires right after registerNetworkCallback; until the engine is actually
// running there is nothing to restart.
public ConcreteNetworkAvailabilityListener(BooleanSupplier shouldNotify) {
this.availableNetworkTypes = new ConcurrentHashMap<>();
this.shouldNotify = shouldNotify;
}

@Override
Expand Down Expand Up @@ -38,9 +49,14 @@ public void onNetworkLost(@Constants.NetworkType int networkType) {
}

private void notifyListener() {
if (listener != null) {
listener.onNetworkTypeChanged();
NetworkToggleListener l = listener;
if (l == null) {
return;
}
if (!shouldNotify.getAsBoolean()) {
return;
}
l.onNetworkTypeChanged();
}

public void subscribe(NetworkToggleListener listener) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,22 @@
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

public class NetworkChangeDetector {
private static final String LOGTAG = NetworkChangeDetector.class.getSimpleName();
private final ConnectivityManager connectivityManager;
private ConnectivityManager.NetworkCallback networkCallback;
private ConnectivityManager.NetworkCallback defaultNetworkCallback;
private volatile NetworkAvailabilityListener listener;
private final AtomicBoolean defaultNetworkCallbackActive = new AtomicBoolean(false);
private final AtomicReference<Network> currentlyBoundDefaultNetwork = new AtomicReference<>(null);

public NetworkChangeDetector(ConnectivityManager connectivityManager) {
this.connectivityManager = connectivityManager;
initNetworkCallback();
initDefaultNetworkCallback();
}

private void checkNetworkCapabilities(Network network, Consumer<Integer> operation) {
Expand Down Expand Up @@ -58,18 +65,74 @@ public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapa
};
}

private void initDefaultNetworkCallback() {
defaultNetworkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(@NonNull Network network) {
if (!defaultNetworkCallbackActive.get()) {
Log.d(LOGTAG, "ignoring onAvailable for " + network + "; default callback inactive");
return;
}
Log.d(LOGTAG, "default network became " + network + ", binding process to it");
try {
if (connectivityManager.bindProcessToNetwork(network)) {
currentlyBoundDefaultNetwork.set(network);
} else {
Log.w(LOGTAG, "bindProcessToNetwork returned false for " + network);
}
} catch (Exception e) {
Log.e(LOGTAG, "bindProcessToNetwork failed", e);
}
}

@Override
public void onLost(@NonNull Network network) {
if (!defaultNetworkCallbackActive.get()) {
Log.d(LOGTAG, "ignoring onLost for " + network + "; default callback inactive");
return;
}
if (!network.equals(currentlyBoundDefaultNetwork.get())) {
Log.d(LOGTAG, "ignoring onLost for " + network + "; not the currently bound default network");
return;
}
Log.d(LOGTAG, "default network " + network + " lost, clearing process binding");
try {
if (connectivityManager.bindProcessToNetwork(null)) {
currentlyBoundDefaultNetwork.compareAndSet(network, null);
}
} catch (Exception e) {
Log.e(LOGTAG, "bindProcessToNetwork(null) failed", e);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public void registerNetworkCallback() {
NetworkRequest.Builder builder = new NetworkRequest.Builder();
builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
connectivityManager.registerNetworkCallback(builder.build(), networkCallback);
defaultNetworkCallbackActive.set(true);
connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public void unregisterNetworkCallback() {
defaultNetworkCallbackActive.set(false);
try {
connectivityManager.unregisterNetworkCallback(networkCallback);
} catch (Exception e) {
Log.e(LOGTAG, "failed to unregister network callback", e);
}
try {
connectivityManager.unregisterNetworkCallback(defaultNetworkCallback);
} catch (Exception e) {
Log.e(LOGTAG, "failed to unregister default network callback", e);
}
try {
connectivityManager.bindProcessToNetwork(null);
currentlyBoundDefaultNetwork.set(null);
} catch (Exception e) {
Log.e(LOGTAG, "bindProcessToNetwork(null) on unregister failed", e);
}
}

public void subscribe(NetworkAvailabilityListener listener) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void deactivateMobile() {
this.listener.onNetworkLost(Constants.NetworkType.MOBILE);
}
}

private static class MockNetworkToggleListener implements NetworkToggleListener {
private int totalTimesNetworkTypeChanged = 0;

Expand All @@ -47,7 +47,7 @@ public void resetCounter() {
public void shouldNotifyListenerNetworkUpgraded() {
// Assemble:
var networkToggleListener = new MockNetworkToggleListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener(() -> true);
networkAvailabilityListener.subscribe(networkToggleListener);

var networkChangeDetector = new MockNetworkChangeDetector(networkAvailabilityListener);
Expand All @@ -64,7 +64,7 @@ public void shouldNotifyListenerNetworkUpgraded() {
public void shouldNotifyListenerNetworkDowngraded() {
// Assemble:
var networkToggleListener = new MockNetworkToggleListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener(() -> true);
networkAvailabilityListener.subscribe(networkToggleListener);

var networkChangeDetector = new MockNetworkChangeDetector(networkAvailabilityListener);
Expand All @@ -82,7 +82,7 @@ public void shouldNotifyListenerNetworkDowngraded() {
public void shouldNotNotifyListenerNetworkDidNotUpgrade() {
// Assemble:
var networkToggleListener = new MockNetworkToggleListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener(() -> true);
networkAvailabilityListener.subscribe(networkToggleListener);

var networkChangeDetector = new MockNetworkChangeDetector(networkAvailabilityListener);
Expand All @@ -103,7 +103,7 @@ public void shouldNotNotifyListenerNetworkDidNotUpgrade() {
public void shouldNotNotifyListenerNoNetworksAvailable() {
// Assemble:
var networkToggleListener = new MockNetworkToggleListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener(() -> true);
networkAvailabilityListener.subscribe(networkToggleListener);

var networkChangeDetector = new MockNetworkChangeDetector(networkAvailabilityListener);
Expand All @@ -118,4 +118,23 @@ public void shouldNotNotifyListenerNoNetworksAvailable() {
// Assert:
assertEquals(0, networkToggleListener.totalTimesNetworkTypeChanged);
}

@Test
public void shouldNotNotifyListenerWhenEngineNotRunning() {
// Assemble: engine never running, so initial onAvailable burst from
// Android must not trigger a restart.
var networkToggleListener = new MockNetworkToggleListener();
var networkAvailabilityListener = new ConcreteNetworkAvailabilityListener(() -> false);
networkAvailabilityListener.subscribe(networkToggleListener);

var networkChangeDetector = new MockNetworkChangeDetector(networkAvailabilityListener);

// Act:
networkChangeDetector.activateMobile();
networkChangeDetector.activateWifi();
networkChangeDetector.deactivateWifi();

// Assert:
assertEquals(0, networkToggleListener.totalTimesNetworkTypeChanged);
}
}
Loading