diff --git a/netbird b/netbird index f01c1eea..5b09078d 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit f01c1eea6ac803fff4678abe93f95b4c965e372b +Subproject commit 5b09078da2ac14550741ce8731e7cf4b4a62a728 diff --git a/tool/src/main/java/io/netbird/client/tool/EngineRestarter.java b/tool/src/main/java/io/netbird/client/tool/EngineRestarter.java index 23230adf..f8922a59 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRestarter.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRestarter.java @@ -6,6 +6,7 @@ import android.util.Log; import io.netbird.client.tool.networks.NetworkToggleListener; +import io.netbird.gomobile.android.ConnectionListener; /** *

EngineRestarter restarts the Go engine.

@@ -52,6 +53,7 @@ private void restartEngine() { if (isRestartInProgress) { Log.e(LOGTAG, "engine restart timeout - forcing flag reset"); isRestartInProgress = false; + notifyDisconnected(); } }; @@ -72,6 +74,7 @@ public void onStarted() { @Override public void onStopped() { Log.d(LOGTAG, "engine is stopped, restarting..."); + notifyConnecting(); engineRunner.runWithoutAuth(); } @@ -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; @@ -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 " 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 dfdbf846..2638b28a 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -29,6 +29,7 @@ class EngineRunner { private boolean engineIsRunning = false; Set 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, @@ -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(); diff --git a/tool/src/main/java/io/netbird/client/tool/ForegroundNotification.java b/tool/src/main/java/io/netbird/client/tool/ForegroundNotification.java index 0161af9c..534dfe61 100644 --- a/tool/src/main/java/io/netbird/client/tool/ForegroundNotification.java +++ b/tool/src/main/java/io/netbird/client/tool/ForegroundNotification.java @@ -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(); 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 8494d48d..2dc5f12d 100644 --- a/tool/src/main/java/io/netbird/client/tool/VPNService.java +++ b/tool/src/main/java/io/netbird/client/tool/VPNService.java @@ -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); diff --git a/tool/src/main/java/io/netbird/client/tool/networks/ConcreteNetworkAvailabilityListener.java b/tool/src/main/java/io/netbird/client/tool/networks/ConcreteNetworkAvailabilityListener.java index 8c8cdbce..2957fb41 100644 --- a/tool/src/main/java/io/netbird/client/tool/networks/ConcreteNetworkAvailabilityListener.java +++ b/tool/src/main/java/io/netbird/client/tool/networks/ConcreteNetworkAvailabilityListener.java @@ -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 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 @@ -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) { diff --git a/tool/src/main/java/io/netbird/client/tool/networks/NetworkChangeDetector.java b/tool/src/main/java/io/netbird/client/tool/networks/NetworkChangeDetector.java index f02a09c9..8fdceca3 100644 --- a/tool/src/main/java/io/netbird/client/tool/networks/NetworkChangeDetector.java +++ b/tool/src/main/java/io/netbird/client/tool/networks/NetworkChangeDetector.java @@ -13,11 +13,13 @@ 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; public NetworkChangeDetector(ConnectivityManager connectivityManager) { this.connectivityManager = connectivityManager; initNetworkCallback(); + initDefaultNetworkCallback(); } private void checkNetworkCapabilities(Network network, Consumer operation) { @@ -58,10 +60,37 @@ public void onCapabilitiesChanged(@NonNull Network network, @NonNull NetworkCapa }; } + private void initDefaultNetworkCallback() { + defaultNetworkCallback = new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + Log.d(LOGTAG, "default network became " + network + ", binding process to it"); + try { + if (!connectivityManager.bindProcessToNetwork(network)) { + Log.w(LOGTAG, "bindProcessToNetwork returned false for " + network); + } + } catch (Exception e) { + Log.e(LOGTAG, "bindProcessToNetwork failed", e); + } + } + + @Override + public void onLost(@NonNull Network network) { + Log.d(LOGTAG, "default network " + network + " lost, clearing process binding"); + try { + connectivityManager.bindProcessToNetwork(null); + } catch (Exception e) { + Log.e(LOGTAG, "bindProcessToNetwork(null) failed", e); + } + } + }; + } + public void registerNetworkCallback() { NetworkRequest.Builder builder = new NetworkRequest.Builder(); builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); connectivityManager.registerNetworkCallback(builder.build(), networkCallback); + connectivityManager.registerDefaultNetworkCallback(defaultNetworkCallback); } public void unregisterNetworkCallback() { @@ -70,6 +99,16 @@ public void unregisterNetworkCallback() { } 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); + } catch (Exception e) { + Log.e(LOGTAG, "bindProcessToNetwork(null) on unregister failed", e); + } } public void subscribe(NetworkAvailabilityListener listener) { diff --git a/tool/src/test/java/io/netbird/client/tool/ConcreteNetworkAvailabilityListenerUnitTest.java b/tool/src/test/java/io/netbird/client/tool/ConcreteNetworkAvailabilityListenerUnitTest.java index 809ceb11..e1ea8c95 100644 --- a/tool/src/test/java/io/netbird/client/tool/ConcreteNetworkAvailabilityListenerUnitTest.java +++ b/tool/src/test/java/io/netbird/client/tool/ConcreteNetworkAvailabilityListenerUnitTest.java @@ -29,7 +29,7 @@ public void deactivateMobile() { this.listener.onNetworkLost(Constants.NetworkType.MOBILE); } } - + private static class MockNetworkToggleListener implements NetworkToggleListener { private int totalTimesNetworkTypeChanged = 0; @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); + } }