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..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 @@ -2,36 +2,36 @@ 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 java.util.UUID; - 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; +import androidx.lifecycle.viewmodel.CreationExtras; +import androidx.lifecycle.viewmodel.MutableCreationExtras; import io.netbird.client.R; import io.netbird.client.ServiceAccessor; import io.netbird.client.databinding.FragmentServerBinding; -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,53 +50,130 @@ public void onAttach(@NonNull Context context) { } } + private void mapStateToUi(ChangeServerFragmentUiState uiState) { + if (uiState.shouldDisplayWarningDialog) { + showConfirmChangeServerDialog(); + } + + 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(); + } + + 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.isUrlInvalid) { + binding.editTextServer.setError(requireContext().getString(R.string.change_server_error_invalid_server_url)); + binding.editTextServer.requestFocus(); + } + + if (uiState.isOperationSuccessful) { + showSuccessDialog(requireContext()); + } + } + + private void setBounds(Drawable drawable) { + if (drawable == null) return; + + var metrics = getResources().getDisplayMetrics(); + int dpValue = 12; + int pixelValue = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, metrics); + + drawable.setBounds(0, 0, pixelValue, pixelValue); + } + + private void clearErrorFlags() { + binding.editTextServer.setError(null); + binding.editTextSetupKey.setError(null); + } + + @NonNull + @Override + public CreationExtras getDefaultViewModelCreationExtras() { + final var defaultExtras = super.getDefaultViewModelCreationExtras(); + + 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); - boolean hideAlert = false; - if (getArguments() != null) { - hideAlert = getArguments().getBoolean("hideAlert", false); - } + viewModel = new ViewModelProvider(this, + ViewModelProvider.Factory.from(ChangeServerFragmentViewModel.initializer)) + .get(ChangeServerFragmentViewModel.class); - if (!hideAlert) { - showConfirmChangeServerDialog(); - } + viewModel.getUiState().observe(getViewLifecycleOwner(), this::mapStateToUi); - binding.btnUseNetbird.setOnClickListener(v -> { - disableUIElements(); - binding.editTextServer.setText(Preferences.defaultServer()); - updateServer(view.getContext(), Preferences.defaultServer()); + Drawable minusIcon = ContextCompat.getDrawable(requireContext(), R.drawable.remove_24px); + Drawable plusIcon = ContextCompat.getDrawable(requireContext(), R.drawable.add_24px); + 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); + + binding.editTextSetupKey.setText(""); + binding.editTextSetupKey.setError(null); + binding.setupKeyGroup.setVisibility(View.GONE); + } else { + binding.textSetupKeyLabel.setCompoundDrawables(minusIcon, null, null, null); + binding.setupKeyGroup.setVisibility(View.VISIBLE); + } }); - 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); } }); } @@ -140,111 +217,19 @@ 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; + 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; @@ -258,13 +243,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 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..4dadd5a --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentUiState.java @@ -0,0 +1,62 @@ +package io.netbird.client.ui.server; + +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; + this.errorMessage = builder.errorMessage; + } + + 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; + + public Builder isUiEnabled(boolean isUiEnabled) { + this.isUiEnabled = isUiEnabled; + return this; + } + + public Builder isSetupKeyInvalid(boolean isSetupKeyInvalid) { + this.isSetupKeyInvalid = isSetupKeyInvalid; + return this; + } + + public Builder isUrlInvalid(boolean isUrlInvalid) { + this.isUrlInvalid = isUrlInvalid; + 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..d23d949 --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/server/ChangeServerFragmentViewModel.java @@ -0,0 +1,182 @@ +package io.netbird.client.ui.server; + +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; + +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 deviceName; + private final Operation stopEngineCommand; + + public ChangeServerFragmentViewModel(String configFilePath, String deviceName, Operation stopEngineCommand) { + this.configFilePath = configFilePath; + this.deviceName = deviceName; + this.stopEngineCommand = stopEngineCommand; + + var state = new ChangeServerFragmentUiState.Builder() + .isUiEnabled(true) + .shouldDisplayWarningDialog(true) + .build(); + 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 isSetupKeyInvalid(String setupKey) { + if (setupKey == null || setupKey.length() != 36) { + return true; + } + + try { + UUID.fromString(setupKey); + } catch (IllegalArgumentException e) { + return true; + } + + return false; + } + + private boolean isUrlInvalid(String url) { + if (!url.matches("^https://.*")) return true; + + try { + new URL(url); + return false; + } catch (MalformedURLException e) { + return true; + } + + } + + 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(); + + 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) { + emitErrorState(e); + } + + @Override + public void onSuccess(boolean isSSOEnabled) { + stopEngineCommand.execute(); + emitSuccessState(); + } + })); + } + + public void loginWithSetupKey(String managementServerAddress, String setupKey) { + disableUi(); + + boolean isUrlInvalid = isUrlInvalid(managementServerAddress); + boolean isSetupKeyInvalid = isSetupKeyInvalid(setupKey); + + if (isUrlInvalid || isSetupKeyInvalid) { + emitErrorState(new ChangeServerFragmentUiState.Builder() + .isUrlInvalid(isUrlInvalid) + .isSetupKeyInvalid(isSetupKeyInvalid) + .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)); + } +} 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 de2a570..9e39701 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 @@ + tools:visibility="visible" + app:constraint_referenced_ids="edit_text_setup_key, text_setup_key_tooltip" /> + + + + + +