From c1b48fbc7774bdf9ff8af42764f508beed0635e6 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Tue, 18 Nov 2025 16:07:35 -0300
Subject: [PATCH 01/16] Fix hint color
---
app/src/main/res/layout/fragment_server.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/src/main/res/layout/fragment_server.xml b/app/src/main/res/layout/fragment_server.xml
index de2a570..452a5b4 100644
--- a/app/src/main/res/layout/fragment_server.xml
+++ b/app/src/main/res/layout/fragment_server.xml
@@ -63,6 +63,7 @@
android:inputType="textUri"
android:background="@drawable/edit_text_white"
android:padding="12dp"
+ android:textColorHint="@color/nb_txt_light"
app:layout_constraintTop_toBottomOf="@id/text_setup_key_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
From 0295aa02e23346ca9f0b1716c1e0effbff2c058b Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Tue, 18 Nov 2025 16:20:55 -0300
Subject: [PATCH 02/16] Fix setup key color
---
app/src/main/res/layout/fragment_server.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/src/main/res/layout/fragment_server.xml b/app/src/main/res/layout/fragment_server.xml
index 452a5b4..3f12434 100644
--- a/app/src/main/res/layout/fragment_server.xml
+++ b/app/src/main/res/layout/fragment_server.xml
@@ -63,6 +63,7 @@
android:inputType="textUri"
android:background="@drawable/edit_text_white"
android:padding="12dp"
+ android:textColor="@color/nb_txt"
android:textColorHint="@color/nb_txt_light"
app:layout_constraintTop_toBottomOf="@id/text_setup_key_label"
app:layout_constraintStart_toStartOf="parent"
From 6d2d6669e744f70aca4cac99da68796e10f8bcff Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Tue, 18 Nov 2025 16:21:51 -0300
Subject: [PATCH 03/16] Add "(Optional)" to Setup key field's title
---
app/src/main/res/values/strings.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 65ae49f..fa5ee14 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -48,7 +48,7 @@
Server
https://example-api.domain.com:443
- Setup key
+ Setup key (Optional)
key
Invalid setup key format
From 95b5c1428b1d1421bc7b9f1272c6089f94bd0b8f Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Tue, 18 Nov 2025 18:08:58 -0300
Subject: [PATCH 04/16] Add ChangeServerFragment UiState and ViewModel
---
.../server/ChangeServerFragmentUiState.java | 54 +++++++
.../server/ChangeServerFragmentViewModel.java | 134 ++++++++++++++++++
2 files changed, 188 insertions(+)
create mode 100644 app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java
create mode 100644 app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java
new file mode 100644
index 0000000..51e8637
--- /dev/null
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java
@@ -0,0 +1,54 @@
+package io.netbird.client.ui.server;
+
+public class ChangeServerFragmentUiState {
+ public final boolean isUiEnabled;
+ public final boolean isSetupKeyInvalid;
+ public final boolean isOperationSuccessful;
+ public final String errorMessage;
+ public final boolean shouldDisplayWarningDialog;
+
+ public ChangeServerFragmentUiState(Builder builder) {
+ this.isSetupKeyInvalid = builder.isSetupKeyInvalid;
+ this.isUiEnabled = builder.isUiEnabled;
+ this.isOperationSuccessful = builder.isOperationSuccessful;
+ this.shouldDisplayWarningDialog = builder.shouldDisplayWarningDialog;
+ this.errorMessage = builder.errorMessage;
+ }
+
+ public static class Builder {
+ private boolean isUiEnabled = true;
+ private boolean isSetupKeyInvalid = false;
+ private boolean isOperationSuccessful = false;
+ private String errorMessage;
+ private boolean shouldDisplayWarningDialog = false;
+
+ public Builder isUiEnabled(boolean isUiEnabled) {
+ this.isUiEnabled = isUiEnabled;
+ return this;
+ }
+
+ public Builder isSetupKeyInvalid(boolean isSetupKeyInvalid) {
+ this.isSetupKeyInvalid = isSetupKeyInvalid;
+ return this;
+ }
+
+ public Builder isOperationSuccessful(boolean isOperationSuccessful) {
+ this.isOperationSuccessful = isOperationSuccessful;
+ return this;
+ }
+
+ public Builder errorMessage(String errorMessage) {
+ this.errorMessage = errorMessage;
+ return this;
+ }
+
+ public Builder shouldDisplayWarningDialog(boolean shouldDisplayWarningDialog) {
+ this.shouldDisplayWarningDialog = shouldDisplayWarningDialog;
+ return this;
+ }
+
+ public ChangeServerFragmentUiState build() {
+ return new ChangeServerFragmentUiState(this);
+ }
+ }
+}
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
new file mode 100644
index 0000000..f04700a
--- /dev/null
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
@@ -0,0 +1,134 @@
+package io.netbird.client.ui.server;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import io.netbird.gomobile.android.Android;
+import io.netbird.gomobile.android.Auth;
+import io.netbird.gomobile.android.ErrListener;
+import io.netbird.gomobile.android.SSOListener;
+
+public class ChangeServerFragmentViewModel extends ViewModel {
+ public interface Operation {
+ void execute();
+ }
+
+ private final MutableLiveData uiState;
+ private final String configFilePath;
+ private final String defaultManagementServerAddress;
+ private final String deviceName;
+ private final Operation stopEngineCommand;
+
+ public ChangeServerFragmentViewModel(String configFilePath, String defaultManagementServerAddress, String deviceName, Operation stopEngineCommand) {
+ this.configFilePath = configFilePath;
+ this.defaultManagementServerAddress = defaultManagementServerAddress;
+ this.deviceName = deviceName;
+ this.stopEngineCommand = stopEngineCommand;
+
+ var state = new ChangeServerFragmentUiState.Builder()
+ .isUiEnabled(true)
+ .shouldDisplayWarningDialog(true)
+ .build();
+ this.uiState = new MutableLiveData<>(state);
+ }
+
+ private boolean isValidSetupKey(String setupKey) {
+ try {
+ UUID.fromString(setupKey);
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ private Optional getAuthenticator(String managementServerAddress) {
+ Auth authenticator;
+
+ try {
+ authenticator = Android.newAuth(configFilePath, managementServerAddress);
+ } catch (Exception e) {
+ emitErrorState(e);
+ return Optional.empty();
+ }
+
+ return Optional.ofNullable(authenticator);
+ }
+
+ private void emitSuccessState() {
+ var state = new ChangeServerFragmentUiState.Builder()
+ .isUiEnabled(false)
+ .isOperationSuccessful(true)
+ .build();
+
+ uiState.postValue(state);
+ }
+
+ private void emitErrorState(Exception e) {
+ var state = new ChangeServerFragmentUiState.Builder()
+ .isUiEnabled(true)
+ .errorMessage(e.getMessage())
+ .build();
+
+ uiState.postValue(state);
+ }
+
+ private void emitErrorState(ChangeServerFragmentUiState state) {
+ uiState.postValue(state);
+ }
+
+ private void disableUi() {
+ uiState.postValue(new ChangeServerFragmentUiState.Builder()
+ .isUiEnabled(false)
+ .build());
+ }
+
+ public LiveData getUiState() {
+ return uiState;
+ }
+
+ public void changeManagementServerAddress(String managementServerAddress) {
+ disableUi();
+
+ getAuthenticator(managementServerAddress).ifPresent((authenticator) -> authenticator.saveConfigIfSSOSupported(new SSOListener() {
+ @Override
+ public void onError(Exception e) {
+ emitErrorState(e);
+ }
+
+ @Override
+ public void onSuccess(boolean isSSOEnabled) {
+ stopEngineCommand.execute();
+ emitSuccessState();
+ }
+ }));
+ }
+
+ public void loginWithSetupKey(String managementServerAddress, String setupKey) {
+ disableUi();
+
+ if (!isValidSetupKey(setupKey)) {
+ emitErrorState(new ChangeServerFragmentUiState.Builder()
+ .isSetupKeyInvalid(true)
+ .isUiEnabled(true)
+ .build());
+ return;
+ }
+
+ getAuthenticator(managementServerAddress).ifPresent((authenticator) -> authenticator.loginWithSetupKeyAndSaveConfig(new ErrListener() {
+ @Override
+ public void onError(Exception e) {
+ emitErrorState(e);
+ }
+
+ @Override
+ public void onSuccess() {
+ stopEngineCommand.execute();
+ emitSuccessState();
+ }
+ }, setupKey, deviceName));
+ }
+}
From be0527ff78415b1076b8764af7b9d28e169516bb Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Tue, 18 Nov 2025 19:47:39 -0300
Subject: [PATCH 05/16] Change Setup Key group in fragment_server.xml to always
be visible
---
app/src/main/res/layout/fragment_server.xml | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/src/main/res/layout/fragment_server.xml b/app/src/main/res/layout/fragment_server.xml
index 3f12434..58d4a4e 100644
--- a/app/src/main/res/layout/fragment_server.xml
+++ b/app/src/main/res/layout/fragment_server.xml
@@ -38,7 +38,6 @@
android:id="@+id/setup_key_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:visibility="gone"
app:constraint_referenced_ids="text_setup_key_label,edit_text_setup_key" />
Date: Tue, 18 Nov 2025 19:48:48 -0300
Subject: [PATCH 06/16] Use ViewModel in ChangeServerFragment
---
.../ui/server/ChangeServerFragment.java | 157 +++++++++++++-----
.../server/ChangeServerFragmentViewModel.java | 21 ++-
2 files changed, 135 insertions(+), 43 deletions(-)
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
index dbdb96c..37dc563 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
@@ -9,29 +9,32 @@
import android.view.View;
import android.view.ViewGroup;
-import java.util.UUID;
-
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.MutableCreationExtras;
+
+import java.util.UUID;
import io.netbird.client.R;
import io.netbird.client.ServiceAccessor;
import io.netbird.client.databinding.FragmentServerBinding;
+import io.netbird.client.tool.Preferences;
import io.netbird.gomobile.android.Android;
import io.netbird.gomobile.android.Auth;
-import io.netbird.client.tool.Preferences;
import io.netbird.gomobile.android.ErrListener;
import io.netbird.gomobile.android.SSOListener;
public class ChangeServerFragment extends Fragment {
- public static final String HideAlertBundleArg="hideAlert";
+ public static final String HideAlertBundleArg = "hideAlert";
private FragmentServerBinding binding;
private ServiceAccessor serviceAccessor;
+ private ChangeServerFragmentViewModel viewModel;
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
@@ -50,55 +53,128 @@ public void onAttach(@NonNull Context context) {
}
}
+ private void mapStateToUi(ChangeServerFragmentUiState uiState) {
+ if (uiState.shouldDisplayWarningDialog) {
+ showConfirmChangeServerDialog();
+ }
+
+ if (uiState.isUiEnabled) {
+ enableUIElements();
+ } else {
+ disableUIElements();
+ }
+
+ if (uiState.errorMessage != null && !uiState.errorMessage.isEmpty()) {
+ binding.editTextServer.setError(uiState.errorMessage);
+ binding.editTextServer.requestFocus();
+ }
+
+ if (uiState.isSetupKeyInvalid) {
+ binding.editTextSetupKey.setError(requireContext().getString(R.string.change_server_error_invalid_setup_key));
+ binding.editTextSetupKey.requestFocus();
+ }
+
+ if (uiState.isOperationSuccessful) {
+ showSuccessDialog(requireContext());
+ }
+ }
+
@SuppressLint("NonConstantResourceId")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
- boolean hideAlert = false;
- if (getArguments() != null) {
- hideAlert = getArguments().getBoolean("hideAlert", false);
- }
+ MutableCreationExtras extras = new MutableCreationExtras();
+ extras.set(ChangeServerFragmentViewModel.CONFIG_FILE_PATH_KEY, Preferences.configFile(requireContext()));
+ extras.set(ChangeServerFragmentViewModel.DEVICE_NAME_KEY, deviceName());
+ extras.set(ChangeServerFragmentViewModel.STOP_ENGINE_COMMAND_KEY,
+ (ChangeServerFragmentViewModel.Operation) () -> serviceAccessor.stopEngine());
- if (!hideAlert) {
- showConfirmChangeServerDialog();
- }
+ viewModel = new ViewModelProvider(getViewModelStore(),
+ ViewModelProvider.Factory.from(ChangeServerFragmentViewModel.initializer), extras)
+ .get(ChangeServerFragmentViewModel.class);
- binding.btnUseNetbird.setOnClickListener(v -> {
- disableUIElements();
- binding.editTextServer.setText(Preferences.defaultServer());
- updateServer(view.getContext(), Preferences.defaultServer());
- });
+ viewModel.getUiState().observe(getViewLifecycleOwner(), this::mapStateToUi);
- binding.btnChangeServer.setOnClickListener(v->{
- if (binding.editTextServer.getText().toString().trim().isEmpty()) {
+ binding.btnChangeServer.setOnClickListener(v -> {
+ String managementServerUri = binding.editTextServer.getText().toString().trim();
+ String setupKey = binding.editTextSetupKey.getText().toString().trim();
+
+ if (managementServerUri.isEmpty() && setupKey.isEmpty()) {
return;
}
- disableUIElements();
+ if (!managementServerUri.isEmpty() && !setupKey.isEmpty()) {
+ viewModel.loginWithSetupKey(managementServerUri, setupKey);
+ } else if (!managementServerUri.isEmpty()) {
+ viewModel.changeManagementServerAddress(managementServerUri);
+ } else {
+ managementServerUri = Preferences.defaultServer();
- if (binding.setupKeyGroup.getVisibility() == View.VISIBLE) {
- String setupKey = binding.editTextSetupKey.getText().toString().trim();
- if(setupKey.isEmpty()) {
- binding.editTextSetupKey.setError(v.getContext().getString(R.string.change_server_error_invalid_setup_key));
- binding.editTextSetupKey.requestFocus();
- enableUIElements();
- return;
- }
- if (!isValidSetupKey(setupKey)) {
- binding.editTextSetupKey.setError(v.getContext().getString(R.string.change_server_error_invalid_setup_key));
- binding.editTextSetupKey.requestFocus();
- enableUIElements();
- return;
- }
- String serverAddress = binding.editTextServer.getText().toString().trim();
- loginWithSetupKey(v.getContext(), serverAddress, setupKey);
+ binding.editTextServer.setText(managementServerUri);
+ viewModel.loginWithSetupKey(managementServerUri, setupKey);
+ }
+ });
+
+ binding.btnUseNetbird.setOnClickListener(v -> {
+ String setupKey = binding.editTextSetupKey.getText().toString().trim();
+ String managementServerUri = Preferences.defaultServer();
+
+ binding.editTextServer.setText(managementServerUri);
+
+ if (setupKey.isEmpty()) {
+ viewModel.changeManagementServerAddress(managementServerUri);
} else {
- // Setup key is empty; update server instead
- String serverAddress = binding.editTextServer.getText().toString().trim();
- updateServer(v.getContext(), serverAddress);
+ viewModel.loginWithSetupKey(managementServerUri, setupKey);
}
});
+
+// boolean hideAlert = false;
+// if (getArguments() != null) {
+// hideAlert = getArguments().getBoolean("hideAlert", false);
+// }
+//
+// if (!hideAlert) {
+// showConfirmChangeServerDialog();
+// }
+//
+// binding.btnUseNetbird.setOnClickListener(v -> {
+// disableUIElements();
+// binding.editTextServer.setText(Preferences.defaultServer());
+// updateServer(view.getContext(), Preferences.defaultServer());
+// });
+//
+// binding.setupKeyGroup.setVisibility(View.VISIBLE);
+//
+// binding.btnChangeServer.setOnClickListener(v -> {
+// if (binding.editTextServer.getText().toString().trim().isEmpty()) {
+// return;
+// }
+//
+// disableUIElements();
+//
+// if (binding.setupKeyGroup.getVisibility() == View.VISIBLE) {
+// String setupKey = binding.editTextSetupKey.getText().toString().trim();
+// if (setupKey.isEmpty()) {
+// binding.editTextSetupKey.setError(v.getContext().getString(R.string.change_server_error_invalid_setup_key));
+// binding.editTextSetupKey.requestFocus();
+// enableUIElements();
+// return;
+// }
+// if (!isValidSetupKey(setupKey)) {
+// binding.editTextSetupKey.setError(v.getContext().getString(R.string.change_server_error_invalid_setup_key));
+// binding.editTextSetupKey.requestFocus();
+// enableUIElements();
+// return;
+// }
+// String serverAddress = binding.editTextServer.getText().toString().trim();
+// loginWithSetupKey(v.getContext(), serverAddress, setupKey);
+// } else {
+// // Setup key is empty; update server instead
+// String serverAddress = binding.editTextServer.getText().toString().trim();
+// updateServer(v.getContext(), serverAddress);
+// }
+// });
}
@Override
@@ -211,7 +287,7 @@ public void onSuccess(boolean sso) {
activity.runOnUiThread(() -> {
if (binding == null) return;
- if(!sso) {
+ if (!sso) {
binding.setupKeyGroup.setVisibility(View.VISIBLE);
} else {
binding.setupKeyGroup.setVisibility(View.GONE);
@@ -238,13 +314,14 @@ public void onSuccess(boolean sso) {
}
private void disableUIElements() {
- if(binding == null) return;
+ if (binding == null) return;
binding.editTextServer.setEnabled(false);
binding.editTextSetupKey.setEnabled(false);
binding.btnChangeServer.setText(R.string.change_server_verifying);
binding.btnChangeServer.setEnabled(false);
binding.btnUseNetbird.setVisibility(View.GONE);
}
+
private void enableUIElements() {
FragmentActivity activity = getActivity();
if (activity == null) return;
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
index f04700a..4d08c66 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
@@ -3,6 +3,8 @@
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.viewmodel.CreationExtras;
+import androidx.lifecycle.viewmodel.ViewModelInitializer;
import java.util.Optional;
import java.util.UUID;
@@ -19,13 +21,11 @@ public interface Operation {
private final MutableLiveData uiState;
private final String configFilePath;
- private final String defaultManagementServerAddress;
private final String deviceName;
private final Operation stopEngineCommand;
- public ChangeServerFragmentViewModel(String configFilePath, String defaultManagementServerAddress, String deviceName, Operation stopEngineCommand) {
+ public ChangeServerFragmentViewModel(String configFilePath, String deviceName, Operation stopEngineCommand) {
this.configFilePath = configFilePath;
- this.defaultManagementServerAddress = defaultManagementServerAddress;
this.deviceName = deviceName;
this.stopEngineCommand = stopEngineCommand;
@@ -36,6 +36,21 @@ public ChangeServerFragmentViewModel(String configFilePath, String defaultManage
this.uiState = new MutableLiveData<>(state);
}
+ public static final CreationExtras.Key CONFIG_FILE_PATH_KEY = new CreationExtras.Key<>() {};
+ public static final CreationExtras.Key DEVICE_NAME_KEY = new CreationExtras.Key<>() {};
+ public static final CreationExtras.Key STOP_ENGINE_COMMAND_KEY = new CreationExtras.Key<>() {};
+
+ static final ViewModelInitializer initializer = new ViewModelInitializer<>(
+ ChangeServerFragmentViewModel.class,
+ creationExtras -> {
+ String configFilePath = creationExtras.get(CONFIG_FILE_PATH_KEY);
+ String deviceName = creationExtras.get(DEVICE_NAME_KEY);
+ Operation stopEngineOperation = creationExtras.get(STOP_ENGINE_COMMAND_KEY);
+
+ return new ChangeServerFragmentViewModel(configFilePath, deviceName, stopEngineOperation);
+ }
+ );
+
private boolean isValidSetupKey(String setupKey) {
try {
UUID.fromString(setupKey);
From f575994c333f5aa5c5821cfa8b3529925642a7e7 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Tue, 18 Nov 2025 22:38:27 -0300
Subject: [PATCH 07/16] Add click listener to textSetupKeyLabel
To toggle editTextSetupKey visibility
Also add plus and minus icons
---
.../ui/server/ChangeServerFragment.java | 35 ++++++++++++++++---
app/src/main/res/drawable/add_24px.xml | 10 ++++++
app/src/main/res/drawable/remove_24px.xml | 10 ++++++
app/src/main/res/layout/fragment_server.xml | 12 ++++---
app/src/main/res/values/strings.xml | 2 +-
5 files changed, 58 insertions(+), 11 deletions(-)
create mode 100644 app/src/main/res/drawable/add_24px.xml
create mode 100644 app/src/main/res/drawable/remove_24px.xml
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
index 37dc563..ac7b4f9 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
@@ -2,6 +2,7 @@
import android.annotation.SuppressLint;
import android.content.Context;
+import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@@ -12,6 +13,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
@@ -79,6 +81,12 @@ private void mapStateToUi(ChangeServerFragmentUiState uiState) {
}
}
+ private void setBounds(Drawable drawable) {
+ if (drawable == null) return;
+
+ drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+ }
+
@SuppressLint("NonConstantResourceId")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
@@ -96,6 +104,23 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
viewModel.getUiState().observe(getViewLifecycleOwner(), this::mapStateToUi);
+ Drawable minusIcon = ContextCompat.getDrawable(requireContext(), R.drawable.remove_24px);
+ Drawable plusIcon = ContextCompat.getDrawable(requireContext(), R.drawable.add_24px);
+ setBounds(minusIcon);
+ setBounds(plusIcon);
+
+ binding.textSetupKeyLabel.setOnClickListener(v -> {
+ if (binding.editTextSetupKey.getVisibility() == View.VISIBLE) {
+ binding.textSetupKeyLabel.setCompoundDrawables(plusIcon, null, null, null);
+
+ binding.editTextSetupKey.setText("");
+ binding.editTextSetupKey.setVisibility(View.GONE);
+ } else {
+ binding.textSetupKeyLabel.setCompoundDrawables(minusIcon, null, null, null);
+ binding.editTextSetupKey.setVisibility(View.VISIBLE);
+ }
+ });
+
binding.btnChangeServer.setOnClickListener(v -> {
String managementServerUri = binding.editTextServer.getText().toString().trim();
String setupKey = binding.editTextSetupKey.getText().toString().trim();
@@ -287,11 +312,11 @@ public void onSuccess(boolean sso) {
activity.runOnUiThread(() -> {
if (binding == null) return;
- if (!sso) {
- binding.setupKeyGroup.setVisibility(View.VISIBLE);
- } else {
- binding.setupKeyGroup.setVisibility(View.GONE);
- }
+// if (!sso) {
+// binding.setupKeyGroup.setVisibility(View.VISIBLE);
+// } else {
+// binding.setupKeyGroup.setVisibility(View.GONE);
+// }
});
enableUIElements();
diff --git a/app/src/main/res/drawable/add_24px.xml b/app/src/main/res/drawable/add_24px.xml
new file mode 100644
index 0000000..627c677
--- /dev/null
+++ b/app/src/main/res/drawable/add_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/remove_24px.xml b/app/src/main/res/drawable/remove_24px.xml
new file mode 100644
index 0000000..e0b42ed
--- /dev/null
+++ b/app/src/main/res/drawable/remove_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/layout/fragment_server.xml b/app/src/main/res/layout/fragment_server.xml
index 58d4a4e..5366de0 100644
--- a/app/src/main/res/layout/fragment_server.xml
+++ b/app/src/main/res/layout/fragment_server.xml
@@ -34,11 +34,11 @@
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="8dp" />
-
+
+
+
+
+
Server
https://example-api.domain.com:443
- Setup key (Optional)
+ Add this device with setup key
key
Invalid setup key format
From 755c0e63b1cf53d2998e75e7668237b39ab46919 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 12:36:03 -0300
Subject: [PATCH 08/16] Fix change_server_setup_key
---
app/src/main/res/values/strings.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b63beda..3c19a83 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -48,7 +48,7 @@
Server
https://example-api.domain.com:443
- Add this device with setup key
+ Add this device with a setup key
key
Invalid setup key format
From c501e17a9a93290a48a740001760f67dada59d69 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 14:16:30 -0300
Subject: [PATCH 09/16] Add usage of view group to toggle setup key widgets'
visibility
---
.../ui/server/ChangeServerFragment.java | 56 ++++++++++++++++++-
app/src/main/res/layout/fragment_server.xml | 47 ++++++++++++----
2 files changed, 90 insertions(+), 13 deletions(-)
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
index ac7b4f9..fe44ffb 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
@@ -1,5 +1,8 @@
package io.netbird.client.ui.server;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
@@ -9,6 +12,7 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.animation.AccelerateDecelerateInterpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -87,6 +91,49 @@ private void setBounds(Drawable drawable) {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
+ public void expandView(final View view) {
+ // Measure the view to get its target height
+ view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ final int targetHeight = view.getMeasuredHeight();
+
+ // Set initial height to 0 and make it visible
+ view.getLayoutParams().height = 0;
+ view.setVisibility(View.VISIBLE);
+
+ ValueAnimator animator = ValueAnimator.ofInt(0, targetHeight);
+ animator.setDuration(1000);
+ animator.setInterpolator(new AccelerateDecelerateInterpolator());
+
+ animator.addUpdateListener(animation -> {
+ view.getLayoutParams().height = (int) animation.getAnimatedValue();
+ view.requestLayout();
+ });
+
+ animator.start();
+ }
+
+ public void collapseView(final View view) {
+ final int initialHeight = view.getMeasuredHeight();
+
+ ValueAnimator animator = ValueAnimator.ofInt(initialHeight, 0);
+ animator.setDuration(1000);
+ animator.setInterpolator(new AccelerateDecelerateInterpolator());
+
+ animator.addUpdateListener(animation -> {
+ view.getLayoutParams().height = (int) animation.getAnimatedValue();
+ view.requestLayout();
+ });
+
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setVisibility(View.GONE); // Hide the view after animation
+ }
+ });
+
+ animator.start();
+ }
+
@SuppressLint("NonConstantResourceId")
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
@@ -110,14 +157,17 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
setBounds(plusIcon);
binding.textSetupKeyLabel.setOnClickListener(v -> {
- if (binding.editTextSetupKey.getVisibility() == View.VISIBLE) {
+ if (binding.setupKeyGroup.getVisibility() == View.VISIBLE) {
binding.textSetupKeyLabel.setCompoundDrawables(plusIcon, null, null, null);
binding.editTextSetupKey.setText("");
- binding.editTextSetupKey.setVisibility(View.GONE);
+ binding.editTextSetupKey.setError(null);
+ binding.setupKeyGroup.setVisibility(View.GONE);
+// collapseView(binding.setupKeyGroup);
} else {
binding.textSetupKeyLabel.setCompoundDrawables(minusIcon, null, null, null);
- binding.editTextSetupKey.setVisibility(View.VISIBLE);
+ binding.setupKeyGroup.setVisibility(View.VISIBLE);
+// expandView(binding.setupKeyGroup);
}
});
diff --git a/app/src/main/res/layout/fragment_server.xml b/app/src/main/res/layout/fragment_server.xml
index 5366de0..32902db 100644
--- a/app/src/main/res/layout/fragment_server.xml
+++ b/app/src/main/res/layout/fragment_server.xml
@@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ xmlns:tools="http://schemas.android.com/tools"
android:padding="16dp">
@@ -20,7 +21,7 @@
-
-
-
-
-
+
+
+
+
+
+
+
From f5c9060eb37867a577ae088dddece2efc8994420 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 20:35:50 -0300
Subject: [PATCH 10/16] Update setup_key hint and warning strings
---
app/src/main/res/values/strings.xml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 3c19a83..1b46f02 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -49,7 +49,7 @@
Server
https://example-api.domain.com:443
Add this device with a setup key
- key
+ "0EF79C2F-DEE1-419B-BFC8-1BF529332998"
Invalid setup key format
Change
@@ -111,4 +111,5 @@
Forces usage of relay when connecting to peers
exclamation mark
To apply the setting, you will need to reconnect.
+ Using setup keys for user devices is not recommended. SSO with MFA provides stronger security, proper user-device association, and periodic re-authentication.
From 7a5ef44c1001a9450bf6f073794f7d7e80d89f5b Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 20:37:00 -0300
Subject: [PATCH 11/16] Change setup key label text size and style
---
app/src/main/res/layout/fragment_server.xml | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/src/main/res/layout/fragment_server.xml b/app/src/main/res/layout/fragment_server.xml
index 32902db..11d51c8 100644
--- a/app/src/main/res/layout/fragment_server.xml
+++ b/app/src/main/res/layout/fragment_server.xml
@@ -48,8 +48,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/change_server_setup_key"
- android:textSize="20sp"
- android:textStyle="bold"
+ android:textSize="12sp"
app:drawableLeftCompat="@drawable/add_24px"
app:layout_constraintTop_toBottomOf="@id/edit_text_server"
app:layout_constraintStart_toStartOf="@id/text_server_label"
From 5cbbf9165d1313ddc7ef58889543ad5d5a1c202c Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 20:37:09 -0300
Subject: [PATCH 12/16] Replace raw string usage with strings file key
---
app/src/main/res/layout/fragment_server.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/src/main/res/layout/fragment_server.xml b/app/src/main/res/layout/fragment_server.xml
index 11d51c8..9e39701 100644
--- a/app/src/main/res/layout/fragment_server.xml
+++ b/app/src/main/res/layout/fragment_server.xml
@@ -89,9 +89,9 @@
From c15332df0e3ae322e4059b7d27c8651dfedf8515 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 20:39:26 -0300
Subject: [PATCH 13/16] Resize add and remove icons
Remove commented and unused code
---
.../ui/server/ChangeServerFragment.java | 213 +-----------------
1 file changed, 8 insertions(+), 205 deletions(-)
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
index fe44ffb..48c482e 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
@@ -1,18 +1,14 @@
package io.netbird.client.ui.server;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
-import android.util.Log;
+import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.view.animation.AccelerateDecelerateInterpolator;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -23,16 +19,10 @@
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.viewmodel.MutableCreationExtras;
-import java.util.UUID;
-
import io.netbird.client.R;
import io.netbird.client.ServiceAccessor;
import io.netbird.client.databinding.FragmentServerBinding;
import io.netbird.client.tool.Preferences;
-import io.netbird.gomobile.android.Android;
-import io.netbird.gomobile.android.Auth;
-import io.netbird.gomobile.android.ErrListener;
-import io.netbird.gomobile.android.SSOListener;
public class ChangeServerFragment extends Fragment {
@@ -88,50 +78,11 @@ private void mapStateToUi(ChangeServerFragmentUiState uiState) {
private void setBounds(Drawable drawable) {
if (drawable == null) return;
- drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
- }
-
- public void expandView(final View view) {
- // Measure the view to get its target height
- view.measure(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- final int targetHeight = view.getMeasuredHeight();
-
- // Set initial height to 0 and make it visible
- view.getLayoutParams().height = 0;
- view.setVisibility(View.VISIBLE);
-
- ValueAnimator animator = ValueAnimator.ofInt(0, targetHeight);
- animator.setDuration(1000);
- animator.setInterpolator(new AccelerateDecelerateInterpolator());
-
- animator.addUpdateListener(animation -> {
- view.getLayoutParams().height = (int) animation.getAnimatedValue();
- view.requestLayout();
- });
-
- animator.start();
- }
-
- public void collapseView(final View view) {
- final int initialHeight = view.getMeasuredHeight();
-
- ValueAnimator animator = ValueAnimator.ofInt(initialHeight, 0);
- animator.setDuration(1000);
- animator.setInterpolator(new AccelerateDecelerateInterpolator());
-
- animator.addUpdateListener(animation -> {
- view.getLayoutParams().height = (int) animation.getAnimatedValue();
- view.requestLayout();
- });
-
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- view.setVisibility(View.GONE); // Hide the view after animation
- }
- });
+ var metrics = getResources().getDisplayMetrics();
+ int dpValue = 12;
+ int pixelValue = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, metrics);
- animator.start();
+ drawable.setBounds(0, 0, pixelValue, pixelValue);
}
@SuppressLint("NonConstantResourceId")
@@ -156,6 +107,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
setBounds(minusIcon);
setBounds(plusIcon);
+ // This is done to resize the first time the plus icon is shown on the label.
+ binding.textSetupKeyLabel.setCompoundDrawables(plusIcon, null, null, null);
+
binding.textSetupKeyLabel.setOnClickListener(v -> {
if (binding.setupKeyGroup.getVisibility() == View.VISIBLE) {
binding.textSetupKeyLabel.setCompoundDrawables(plusIcon, null, null, null);
@@ -163,11 +117,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
binding.editTextSetupKey.setText("");
binding.editTextSetupKey.setError(null);
binding.setupKeyGroup.setVisibility(View.GONE);
-// collapseView(binding.setupKeyGroup);
} else {
binding.textSetupKeyLabel.setCompoundDrawables(minusIcon, null, null, null);
binding.setupKeyGroup.setVisibility(View.VISIBLE);
-// expandView(binding.setupKeyGroup);
}
});
@@ -203,53 +155,6 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
viewModel.loginWithSetupKey(managementServerUri, setupKey);
}
});
-
-// boolean hideAlert = false;
-// if (getArguments() != null) {
-// hideAlert = getArguments().getBoolean("hideAlert", false);
-// }
-//
-// if (!hideAlert) {
-// showConfirmChangeServerDialog();
-// }
-//
-// binding.btnUseNetbird.setOnClickListener(v -> {
-// disableUIElements();
-// binding.editTextServer.setText(Preferences.defaultServer());
-// updateServer(view.getContext(), Preferences.defaultServer());
-// });
-//
-// binding.setupKeyGroup.setVisibility(View.VISIBLE);
-//
-// binding.btnChangeServer.setOnClickListener(v -> {
-// if (binding.editTextServer.getText().toString().trim().isEmpty()) {
-// return;
-// }
-//
-// disableUIElements();
-//
-// if (binding.setupKeyGroup.getVisibility() == View.VISIBLE) {
-// String setupKey = binding.editTextSetupKey.getText().toString().trim();
-// if (setupKey.isEmpty()) {
-// binding.editTextSetupKey.setError(v.getContext().getString(R.string.change_server_error_invalid_setup_key));
-// binding.editTextSetupKey.requestFocus();
-// enableUIElements();
-// return;
-// }
-// if (!isValidSetupKey(setupKey)) {
-// binding.editTextSetupKey.setError(v.getContext().getString(R.string.change_server_error_invalid_setup_key));
-// binding.editTextSetupKey.requestFocus();
-// enableUIElements();
-// return;
-// }
-// String serverAddress = binding.editTextServer.getText().toString().trim();
-// loginWithSetupKey(v.getContext(), serverAddress, setupKey);
-// } else {
-// // Setup key is empty; update server instead
-// String serverAddress = binding.editTextServer.getText().toString().trim();
-// updateServer(v.getContext(), serverAddress);
-// }
-// });
}
@Override
@@ -291,103 +196,10 @@ private void showSuccessDialog(Context context) {
});
}
- private void loginWithSetupKey(Context context, String mgmServerAddress, String setupKey) {
- String configFilePath = Preferences.configFile(context);
- try {
- Auth auther = Android.newAuth(configFilePath, mgmServerAddress);
- auther.loginWithSetupKeyAndSaveConfig(new ErrListener() {
- @Override
- public void onError(Exception e) {
- FragmentActivity activity = getActivity();
- if (activity == null) return;
- activity.runOnUiThread(() -> {
- if (binding == null) return;
- binding.editTextServer.setError(e.getMessage());
- binding.editTextServer.requestFocus();
- });
- enableUIElements();
- }
-
- @Override
- public void onSuccess() {
- enableUIElements();
- showSuccessDialog(context);
- serviceAccessor.stopEngine();
- }
- }, setupKey, deviceName());
- } catch (Exception e) {
- FragmentActivity activity = getActivity();
- if (activity == null) return;
- activity.runOnUiThread(() -> {
- if (binding == null) return;
-
- binding.editTextServer.setError(e.getMessage());
- binding.editTextServer.requestFocus();
- });
- enableUIElements();
- }
- }
-
private String deviceName() {
return Build.PRODUCT;
}
- private void updateServer(Context context, String mgmServerAddress) {
- String configFilePath = Preferences.configFile(context);
- try {
- Auth auther = Android.newAuth(configFilePath, mgmServerAddress);
- auther.saveConfigIfSSOSupported(new SSOListener() {
- @Override
- public void onError(Exception e) {
- Log.e("ChangeServerFragment", "Error updating server: " + e.getMessage());
-
- FragmentActivity activity = getActivity();
- if (activity == null) return;
-
- activity.runOnUiThread(() -> {
- if (binding == null) return;
-
- binding.editTextServer.setError(e.getMessage());
- binding.editTextServer.requestFocus();
- });
- enableUIElements();
- }
-
- @Override
- public void onSuccess(boolean sso) {
- FragmentActivity activity = getActivity();
- if (activity == null) return;
-
- Log.i("ChangeServerFragment", "update server result, SSO supported: " + sso);
- activity.runOnUiThread(() -> {
- if (binding == null) return;
-
-// if (!sso) {
-// binding.setupKeyGroup.setVisibility(View.VISIBLE);
-// } else {
-// binding.setupKeyGroup.setVisibility(View.GONE);
-// }
- });
-
- enableUIElements();
- showSuccessDialog(context);
- serviceAccessor.stopEngine();
- }
- });
- } catch (Exception e) {
- Log.e("ChangeServerFragment", "Exception in updating server: " + e.getMessage());
- FragmentActivity activity = getActivity();
- if (activity == null) return;
- activity.runOnUiThread(() -> {
- if (binding == null) return;
-
- binding.editTextServer.setError(e.getMessage());
- binding.editTextServer.requestFocus();
- });
- enableUIElements();
- }
- }
-
private void disableUIElements() {
if (binding == null) return;
binding.editTextServer.setEnabled(false);
@@ -410,13 +222,4 @@ private void enableUIElements() {
binding.btnUseNetbird.setVisibility(View.VISIBLE);
});
}
-
- private boolean isValidSetupKey(String setupKey) {
- try {
- UUID.fromString(setupKey);
- return true;
- } catch (IllegalArgumentException e) {
- return false;
- }
- }
}
\ No newline at end of file
From 35bdf1aca4a6fa4ed81823f91d4adbcc3168bf23 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 21:58:33 -0300
Subject: [PATCH 14/16] Override getDefaultViewModelCreationExtras
To allow usage of ViewModelProvider constructor
that receives the fragment's instance as the
ViewModelStoreOwner (for lifecycle management)
---
.../ui/server/ChangeServerFragment.java | 20 +++++++++++++------
1 file changed, 14 insertions(+), 6 deletions(-)
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
index 48c482e..534ab14 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
@@ -17,6 +17,7 @@
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.lifecycle.viewmodel.MutableCreationExtras;
import io.netbird.client.R;
@@ -85,19 +86,26 @@ private void setBounds(Drawable drawable) {
drawable.setBounds(0, 0, pixelValue, pixelValue);
}
- @SuppressLint("NonConstantResourceId")
+ @NonNull
@Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
+ public CreationExtras getDefaultViewModelCreationExtras() {
+ final var defaultExtras = super.getDefaultViewModelCreationExtras();
- MutableCreationExtras extras = new MutableCreationExtras();
+ final var extras = new MutableCreationExtras(defaultExtras);
extras.set(ChangeServerFragmentViewModel.CONFIG_FILE_PATH_KEY, Preferences.configFile(requireContext()));
extras.set(ChangeServerFragmentViewModel.DEVICE_NAME_KEY, deviceName());
extras.set(ChangeServerFragmentViewModel.STOP_ENGINE_COMMAND_KEY,
(ChangeServerFragmentViewModel.Operation) () -> serviceAccessor.stopEngine());
+ return extras;
+ }
+
+ @SuppressLint("NonConstantResourceId")
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
- viewModel = new ViewModelProvider(getViewModelStore(),
- ViewModelProvider.Factory.from(ChangeServerFragmentViewModel.initializer), extras)
+ viewModel = new ViewModelProvider(this,
+ ViewModelProvider.Factory.from(ChangeServerFragmentViewModel.initializer))
.get(ChangeServerFragmentViewModel.class);
viewModel.getUiState().observe(getViewLifecycleOwner(), this::mapStateToUi);
From 9cf8595d8c21062dc28713ab588df55d005a1316 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 22:35:52 -0300
Subject: [PATCH 15/16] Add server URL validation
Fix UUID validation
---
.../ui/server/ChangeServerFragment.java | 13 +++++++
.../server/ChangeServerFragmentUiState.java | 8 +++++
.../server/ChangeServerFragmentViewModel.java | 35 ++++++++++++++++---
app/src/main/res/values/strings.xml | 1 +
4 files changed, 52 insertions(+), 5 deletions(-)
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
index 534ab14..85b9c44 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragment.java
@@ -58,6 +58,9 @@ private void mapStateToUi(ChangeServerFragmentUiState uiState) {
if (uiState.isUiEnabled) {
enableUIElements();
} else {
+ // clearing error flags here because UI elements are only
+ // disabled when managed to click the button and there are no errors.
+ clearErrorFlags();
disableUIElements();
}
@@ -71,6 +74,11 @@ private void mapStateToUi(ChangeServerFragmentUiState uiState) {
binding.editTextSetupKey.requestFocus();
}
+ if (uiState.isUrlInvalid) {
+ binding.editTextServer.setError(requireContext().getString(R.string.change_server_error_invalid_server_url));
+ binding.editTextServer.requestFocus();
+ }
+
if (uiState.isOperationSuccessful) {
showSuccessDialog(requireContext());
}
@@ -86,6 +94,11 @@ private void setBounds(Drawable drawable) {
drawable.setBounds(0, 0, pixelValue, pixelValue);
}
+ private void clearErrorFlags() {
+ binding.editTextServer.setError(null);
+ binding.editTextSetupKey.setError(null);
+ }
+
@NonNull
@Override
public CreationExtras getDefaultViewModelCreationExtras() {
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java
index 51e8637..4dadd5a 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java
@@ -3,12 +3,14 @@
public class ChangeServerFragmentUiState {
public final boolean isUiEnabled;
public final boolean isSetupKeyInvalid;
+ public final boolean isUrlInvalid;
public final boolean isOperationSuccessful;
public final String errorMessage;
public final boolean shouldDisplayWarningDialog;
public ChangeServerFragmentUiState(Builder builder) {
this.isSetupKeyInvalid = builder.isSetupKeyInvalid;
+ this.isUrlInvalid = builder.isUrlInvalid;
this.isUiEnabled = builder.isUiEnabled;
this.isOperationSuccessful = builder.isOperationSuccessful;
this.shouldDisplayWarningDialog = builder.shouldDisplayWarningDialog;
@@ -18,6 +20,7 @@ public ChangeServerFragmentUiState(Builder builder) {
public static class Builder {
private boolean isUiEnabled = true;
private boolean isSetupKeyInvalid = false;
+ private boolean isUrlInvalid = false;
private boolean isOperationSuccessful = false;
private String errorMessage;
private boolean shouldDisplayWarningDialog = false;
@@ -32,6 +35,11 @@ public Builder isSetupKeyInvalid(boolean isSetupKeyInvalid) {
return this;
}
+ public Builder isUrlInvalid(boolean isUrlInvalid) {
+ this.isUrlInvalid = isUrlInvalid;
+ return this;
+ }
+
public Builder isOperationSuccessful(boolean isOperationSuccessful) {
this.isOperationSuccessful = isOperationSuccessful;
return this;
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
index 4d08c66..9f70ae9 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
@@ -1,5 +1,7 @@
package io.netbird.client.ui.server;
+import android.util.Log;
+
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
@@ -51,13 +53,22 @@ public ChangeServerFragmentViewModel(String configFilePath, String deviceName, O
}
);
- private boolean isValidSetupKey(String setupKey) {
+ private boolean isSetupKeyInvalid(String setupKey) {
+ if (setupKey == null || setupKey.length() != 36) {
+ return true;
+ }
+
try {
UUID.fromString(setupKey);
- return true;
} catch (IllegalArgumentException e) {
- return false;
+ return true;
}
+
+ return false;
+ }
+
+ private boolean isUrlInvalid(String url) {
+ return !url.matches("^https://.*");
}
private Optional getAuthenticator(String managementServerAddress) {
@@ -108,6 +119,15 @@ public LiveData getUiState() {
public void changeManagementServerAddress(String managementServerAddress) {
disableUi();
+ if (isUrlInvalid(managementServerAddress)) {
+ emitErrorState(new ChangeServerFragmentUiState.Builder()
+ .isUrlInvalid(true)
+ .isUiEnabled(true)
+ .build());
+
+ return;
+ }
+
getAuthenticator(managementServerAddress).ifPresent((authenticator) -> authenticator.saveConfigIfSSOSupported(new SSOListener() {
@Override
public void onError(Exception e) {
@@ -125,11 +145,16 @@ public void onSuccess(boolean isSSOEnabled) {
public void loginWithSetupKey(String managementServerAddress, String setupKey) {
disableUi();
- if (!isValidSetupKey(setupKey)) {
+ boolean isUrlInvalid = isUrlInvalid(managementServerAddress);
+ boolean isSetupKeyInvalid = isSetupKeyInvalid(setupKey);
+
+ if (isUrlInvalid || isSetupKeyInvalid) {
emitErrorState(new ChangeServerFragmentUiState.Builder()
- .isSetupKeyInvalid(true)
+ .isUrlInvalid(isUrlInvalid)
+ .isSetupKeyInvalid(isSetupKeyInvalid)
.isUiEnabled(true)
.build());
+
return;
}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1b46f02..cd068c5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -112,4 +112,5 @@
exclamation mark
To apply the setting, you will need to reconnect.
Using setup keys for user devices is not recommended. SSO with MFA provides stronger security, proper user-device association, and periodic re-authentication.
+ Invalid URL format
From 9c860c7d43c111d9514f9267d03130543fa581f8 Mon Sep 17 00:00:00 2001
From: Diego Romar <18450339+doromaraujo@users.noreply.github.com>
Date: Wed, 19 Nov 2025 22:54:35 -0300
Subject: [PATCH 16/16] Improve server URL validation
---
.../ui/server/ChangeServerFragmentViewModel.java | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
index 9f70ae9..d23d949 100644
--- a/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
+++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java
@@ -1,13 +1,13 @@
package io.netbird.client.ui.server;
-import android.util.Log;
-
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.lifecycle.viewmodel.ViewModelInitializer;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.util.Optional;
import java.util.UUID;
@@ -68,7 +68,15 @@ private boolean isSetupKeyInvalid(String setupKey) {
}
private boolean isUrlInvalid(String url) {
- return !url.matches("^https://.*");
+ if (!url.matches("^https://.*")) return true;
+
+ try {
+ new URL(url);
+ return false;
+ } catch (MalformedURLException e) {
+ return true;
+ }
+
}
private Optional getAuthenticator(String managementServerAddress) {