From 449db69f9352fceb1ffbf310e72d151e240f7c2b Mon Sep 17 00:00:00 2001 From: Diego Romar Date: Wed, 10 Sep 2025 14:21:04 -0300 Subject: [PATCH] Add force relay connection option (#77) This adds a new switch control to the advanced settings so that users may opt to force usage of relay when connecting to peers. It also warns the user that this configuration change will only be applied the next time they connect to the VPN. --- .../client/ui/advanced/AdvancedFragment.java | 33 +++++++ .../main/res/drawable/bg_rounded_nb_bg.xml | 6 ++ app/src/main/res/layout/component_switch.xml | 40 ++++++++ .../layout/dialog_simple_alert_message.xml | 44 +++++++++ app/src/main/res/layout/fragment_advanced.xml | 15 ++- app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/themes.xml | 4 + netbird | 2 +- .../tool/EnvVarPackagerInstrumentedTest.java | 27 ++++++ .../client/tool/ExampleInstrumentedTest.java | 2 +- .../tool/PreferencesInstrumentedTest.java | 93 +++++++++++++++++++ .../io/netbird/client/tool/EngineRunner.java | 7 +- .../netbird/client/tool/EnvVarPackager.java | 14 +++ .../io/netbird/client/tool/Preferences.java | 15 +++ 14 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/bg_rounded_nb_bg.xml create mode 100644 app/src/main/res/layout/component_switch.xml create mode 100644 app/src/main/res/layout/dialog_simple_alert_message.xml create mode 100644 tool/src/androidTest/java/io/netbird/client/tool/EnvVarPackagerInstrumentedTest.java create mode 100644 tool/src/androidTest/java/io/netbird/client/tool/PreferencesInstrumentedTest.java create mode 100644 tool/src/main/java/io/netbird/client/tool/EnvVarPackager.java diff --git a/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java b/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java index 5e4a96b..3e5bedb 100644 --- a/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java +++ b/app/src/main/java/io/netbird/client/ui/advanced/AdvancedFragment.java @@ -9,12 +9,16 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.RadioGroup; +import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatDelegate; import androidx.fragment.app.Fragment; +import io.netbird.client.R; +import io.netbird.client.databinding.ComponentSwitchBinding; import io.netbird.client.databinding.FragmentAdvancedBinding; import io.netbird.client.tool.Logcat; import io.netbird.client.tool.Preferences; @@ -28,6 +32,33 @@ public class AdvancedFragment extends Fragment { private FragmentAdvancedBinding binding; private io.netbird.gomobile.android.Preferences goPreferences; + private void showReconnectionNeededWarningDialog() { + final View dialogView = getLayoutInflater().inflate(R.layout.dialog_simple_alert_message, null); + final AlertDialog alertDialog = new AlertDialog.Builder(requireContext(), R.style.AlertDialogTheme) + .setView(dialogView) + .create(); + + ((TextView)dialogView.findViewById(R.id.txt_dialog)).setText(R.string.reconnectionNeededWarningMessage); + dialogView.findViewById(R.id.btn_ok_dialog).setOnClickListener(v -> alertDialog.dismiss()); + alertDialog.show(); + } + + private void configureForceRelayConnectionSwitch(@NonNull ComponentSwitchBinding binding, @NonNull Preferences preferences) { + binding.switchTitle.setText(R.string.advanced_force_relay_conn); + binding.switchDescription.setText(R.string.advanced_force_relay_conn_desc); + + binding.switchControl.setChecked(preferences.isConnectionForceRelayed()); + binding.switchControl.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + preferences.enableForcedRelayConnection(); + } else { + preferences.disableForcedRelayConnection(); + } + + showReconnectionNeededWarningDialog(); + }); + } + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -122,6 +153,8 @@ public class AdvancedFragment extends Fragment { } }); + configureForceRelayConnectionSwitch(binding.layoutForceRelayConnection, preferences); + // Initialize engine config switches (your settings) initializeEngineConfigSwitches(); diff --git a/app/src/main/res/drawable/bg_rounded_nb_bg.xml b/app/src/main/res/drawable/bg_rounded_nb_bg.xml new file mode 100644 index 0000000..25a4b5a --- /dev/null +++ b/app/src/main/res/drawable/bg_rounded_nb_bg.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/component_switch.xml b/app/src/main/res/layout/component_switch.xml new file mode 100644 index 0000000..7c24a32 --- /dev/null +++ b/app/src/main/res/layout/component_switch.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_simple_alert_message.xml b/app/src/main/res/layout/dialog_simple_alert_message.xml new file mode 100644 index 0000000..cd5f1b5 --- /dev/null +++ b/app/src/main/res/layout/dialog_simple_alert_message.xml @@ -0,0 +1,44 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_advanced.xml b/app/src/main/res/layout/fragment_advanced.xml index 4fb5556..23d5013 100644 --- a/app/src/main/res/layout/fragment_advanced.xml +++ b/app/src/main/res/layout/fragment_advanced.xml @@ -5,6 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" + android:id="@+id/scr_vw_advanced" tools:context=".ui.advanced.AdvancedFragment"> + + Dark Light Choose the app appearance mode. + Force relay connection + Forces usage of relay when connecting to peers + exclamation mark + To apply the setting, you will need to reconnect. diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 3f725ed..5a24308 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -57,4 +57,8 @@ @color/switch_track_color + + \ No newline at end of file diff --git a/netbird b/netbird index f425870..a53243e 160000 --- a/netbird +++ b/netbird @@ -1 +1 @@ -Subproject commit f425870c8e76eb4997f225c8114fd9b32696599d +Subproject commit a53243eed0c575d92545ac825ee8c376bf342926 diff --git a/tool/src/androidTest/java/io/netbird/client/tool/EnvVarPackagerInstrumentedTest.java b/tool/src/androidTest/java/io/netbird/client/tool/EnvVarPackagerInstrumentedTest.java new file mode 100644 index 0000000..502eeed --- /dev/null +++ b/tool/src/androidTest/java/io/netbird/client/tool/EnvVarPackagerInstrumentedTest.java @@ -0,0 +1,27 @@ +package io.netbird.client.tool; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; + +import io.netbird.gomobile.android.Android; + +@RunWith(AndroidJUnit4.class) +public class EnvVarPackagerInstrumentedTest { + @Test + public void shouldReturnEnvironmentVariables() { + var preferences = new Preferences(InstrumentationRegistry.getInstrumentation().getTargetContext()); + var environmentVariables = EnvVarPackager.getEnvironmentVariables(preferences); + + Assert.assertNotNull(environmentVariables); + var forceRelay = environmentVariables.get(Android.getEnvKeyNBForceRelay()); + var variableNotPresentInList = environmentVariables.get("UNKNOWN_VAR"); + var emptyString = ""; + + Assert.assertNotEquals(emptyString, forceRelay); + Assert.assertEquals(emptyString, variableNotPresentInList); + } +} diff --git a/tool/src/androidTest/java/io/netbird/client/tool/ExampleInstrumentedTest.java b/tool/src/androidTest/java/io/netbird/client/tool/ExampleInstrumentedTest.java index 1ba806e..af279fb 100644 --- a/tool/src/androidTest/java/io/netbird/client/tool/ExampleInstrumentedTest.java +++ b/tool/src/androidTest/java/io/netbird/client/tool/ExampleInstrumentedTest.java @@ -21,6 +21,6 @@ public class ExampleInstrumentedTest { public void useAppContext() { // Context of the app under test. Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("io.netbird.client.backend.test", appContext.getPackageName()); + assertEquals("io.netbird.client.tool.test", appContext.getPackageName()); } } \ No newline at end of file diff --git a/tool/src/androidTest/java/io/netbird/client/tool/PreferencesInstrumentedTest.java b/tool/src/androidTest/java/io/netbird/client/tool/PreferencesInstrumentedTest.java new file mode 100644 index 0000000..cf7a7f2 --- /dev/null +++ b/tool/src/androidTest/java/io/netbird/client/tool/PreferencesInstrumentedTest.java @@ -0,0 +1,93 @@ +package io.netbird.client.tool; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.After; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import android.content.Context; + +@RunWith(AndroidJUnit4.class) +public class PreferencesInstrumentedTest { + private static Preferences preferences; + + private static Context getContext() { + return InstrumentationRegistry.getInstrumentation().getTargetContext(); + } + + @BeforeClass + public static void setUp() { + preferences = new Preferences(getContext()); + } + + @After + public void tearDown() { + getContext().getSharedPreferences("netbird", Context.MODE_PRIVATE).edit().clear().apply(); + } + + @Test + public void shouldCreatePreferencesWithoutThrownException() { + Preferences preferences = null; + Exception thrown = null; + + try { + preferences = new Preferences(getContext()); + } catch (Exception e) { + thrown = e; + } + + Assert.assertNull(thrown); + Assert.assertNotNull(preferences); + } + + @Test + public void shouldReturnFalseWhenConnectionForceRelayedIsNotSet() { + Assert.assertFalse(preferences.isConnectionForceRelayed()); + } + + @Test + public void shouldReturnTrueAfterEnablingForcedRelayConnection() { + preferences.enableForcedRelayConnection(); + + Assert.assertTrue(preferences.isConnectionForceRelayed()); + } + + @Test + public void shouldReturnFalseAfterDisablingForcedRelayConnection() { + preferences.enableForcedRelayConnection(); + preferences.disableForcedRelayConnection(); + + Assert.assertFalse(preferences.isConnectionForceRelayed()); + } + + @Test + public void shouldReturnFalseWhenTraceLogIsNotSet() { + Assert.assertFalse(preferences.isTraceLogEnabled()); + } + + @Test + public void shouldReturnTrueAfterEnablingTraceLog() { + preferences.enableTraceLog(); + + Assert.assertTrue(preferences.isTraceLogEnabled()); + } + + @Test + public void shouldReturnFalseAfterDisablingTraceLog() { + preferences.enableTraceLog(); + preferences.disableTraceLog(); + + Assert.assertFalse(preferences.isTraceLogEnabled()); + } + + @Test + public void shouldReturnCorrectDefaultServer() { + final var defaultServer = "https://api.netbird.io"; + + Assert.assertEquals(defaultServer, Preferences.defaultServer()); + } +} 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 331d329..55ec345 100644 --- a/tool/src/main/java/io/netbird/client/tool/EngineRunner.java +++ b/tool/src/main/java/io/netbird/client/tool/EngineRunner.java @@ -60,12 +60,15 @@ class EngineRunner { 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) { - goClient.runWithoutLogin(dnsWatch.dnsServers(), () -> dnsWatch.setDNSChangeListener(this::changed)); + goClient.runWithoutLogin(dnsWatch.dnsServers(), () -> dnsWatch.setDNSChangeListener(this::changed), envList); } else { - goClient.run(urlOpener, dnsWatch.dnsServers(), () -> dnsWatch.setDNSChangeListener(this::changed)); + goClient.run(urlOpener, dnsWatch.dnsServers(), () -> dnsWatch.setDNSChangeListener(this::changed), envList); } } catch (Exception e) { Log.e(LOGTAG, "goClient error", e); diff --git a/tool/src/main/java/io/netbird/client/tool/EnvVarPackager.java b/tool/src/main/java/io/netbird/client/tool/EnvVarPackager.java new file mode 100644 index 0000000..f6413e9 --- /dev/null +++ b/tool/src/main/java/io/netbird/client/tool/EnvVarPackager.java @@ -0,0 +1,14 @@ +package io.netbird.client.tool; + +import io.netbird.gomobile.android.Android; +import io.netbird.gomobile.android.EnvList; + +public class EnvVarPackager { + public static EnvList getEnvironmentVariables(Preferences preferences) { + var envList = new EnvList(); + + envList.put(Android.getEnvKeyNBForceRelay(), String.valueOf(preferences.isConnectionForceRelayed())); + + return envList; + } +} 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 e35b29d..1f9c7b7 100644 --- a/tool/src/main/java/io/netbird/client/tool/Preferences.java +++ b/tool/src/main/java/io/netbird/client/tool/Preferences.java @@ -6,6 +6,9 @@ import android.content.SharedPreferences; public class Preferences { private final String keyTraceLog = "tracelog"; + + private final String keyForceRelayConnection = "isConnectionForceRelayed"; + private final SharedPreferences sharedPref; public static String configFile(Context context){ @@ -27,6 +30,18 @@ public class Preferences { sharedPref.edit().putBoolean(keyTraceLog, false).apply(); } + public boolean isConnectionForceRelayed() { + return sharedPref.getBoolean(keyForceRelayConnection, false); + } + + public void enableForcedRelayConnection() { + sharedPref.edit().putBoolean(keyForceRelayConnection, true).apply(); + } + + public void disableForcedRelayConnection() { + sharedPref.edit().putBoolean(keyForceRelayConnection, false).apply(); + } + public static String defaultServer() { return "https://api.netbird.io"; }