diff --git a/app/src/main/java/io/netbird/client/PeersStateListener.java b/app/src/main/java/io/netbird/client/PeersStateListener.java new file mode 100644 index 00000000..3075adf6 --- /dev/null +++ b/app/src/main/java/io/netbird/client/PeersStateListener.java @@ -0,0 +1,5 @@ +package io.netbird.client; + +public interface PeersStateListener { + void onPeersChanged(long totalPeers); +} diff --git a/app/src/main/java/io/netbird/client/PeersStateListenerAdapter.java b/app/src/main/java/io/netbird/client/PeersStateListenerAdapter.java new file mode 100644 index 00000000..c5a5b1d3 --- /dev/null +++ b/app/src/main/java/io/netbird/client/PeersStateListenerAdapter.java @@ -0,0 +1,22 @@ +package io.netbird.client; + +import org.jetbrains.annotations.NotNull; + +public class PeersStateListenerAdapter extends StateListenerAdapter { + private PeersStateListener listener; + + public PeersStateListenerAdapter(@NotNull PeersStateListener listener) { + this.listener = listener; + } + + public void clearListener() { + this.listener = null; + } + + @Override + public void onPeersListChanged(long totalPeers) { + if (listener == null) return; + + listener.onPeersChanged(totalPeers); + } +} diff --git a/app/src/main/java/io/netbird/client/StateListenerAdapter.java b/app/src/main/java/io/netbird/client/StateListenerAdapter.java new file mode 100644 index 00000000..6b952b2e --- /dev/null +++ b/app/src/main/java/io/netbird/client/StateListenerAdapter.java @@ -0,0 +1,44 @@ +package io.netbird.client; + +public abstract class StateListenerAdapter implements StateListener { + + @Override + public void onEngineStarted() { + + } + + @Override + public void onEngineStopped() { + + } + + @Override + public void onAddressChanged(String fqdn, String ip) { + + } + + @Override + public void onConnected() { + + } + + @Override + public void onConnecting() { + + } + + @Override + public void onDisconnected() { + + } + + @Override + public void onDisconnecting() { + + } + + @Override + public void onPeersListChanged(long totalPeers) { + + } +} diff --git a/app/src/main/java/io/netbird/client/ui/home/PeersFragment.java b/app/src/main/java/io/netbird/client/ui/home/PeersFragment.java index deaf539e..eb6b28d5 100644 --- a/app/src/main/java/io/netbird/client/ui/home/PeersFragment.java +++ b/app/src/main/java/io/netbird/client/ui/home/PeersFragment.java @@ -1,23 +1,22 @@ package io.netbird.client.ui.home; import android.content.Context; -import android.content.Intent; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Bundle; +import android.text.Editable; import android.text.Html; +import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import android.text.TextWatcher; -import android.text.Editable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -26,16 +25,16 @@ import io.netbird.client.R; import io.netbird.client.ServiceAccessor; +import io.netbird.client.StateListenerRegistry; import io.netbird.client.databinding.FragmentPeersBinding; -import io.netbird.gomobile.android.PeerInfo; -import io.netbird.gomobile.android.PeerInfoArray; public class PeersFragment extends Fragment { private FragmentPeersBinding binding; private ServiceAccessor serviceAccessor; - private RecyclerView peersListView; - + private StateListenerRegistry stateListenerRegistry; + private PeersFragmentViewModel model; + private final List peers = new ArrayList<>(); private static final String ARG_IS_RUNNING_ON_TV = "isRunningOnTV"; @Override @@ -46,6 +45,12 @@ public void onAttach(@NonNull Context context) { } else { throw new RuntimeException(context + " must implement ServiceAccessor"); } + + if (context instanceof StateListenerRegistry) { + stateListenerRegistry = (StateListenerRegistry) context; + } else { + throw new RuntimeException(context + " must implement StateListenerRegistry"); + } } @Override @@ -58,6 +63,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); + model = new ViewModelProvider(this, PeersFragmentViewModel.getFactory(serviceAccessor)) + .get(PeersFragmentViewModel.class); + stateListenerRegistry.registerServiceStateListener(model.getStateListener()); + boolean isRunningOnTV = false; if (getArguments() != null) { isRunningOnTV = getArguments().getBoolean(ARG_IS_RUNNING_ON_TV, false); @@ -75,15 +84,22 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat ZeroPeerView.setupLearnWhyClick(binding.zeroPeerLayout, requireContext()); } - PeerInfoArray peersInfo = serviceAccessor.getPeersList(); - ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.peersList, peersInfo.size() > 0); + PeersAdapter adapter = new PeersAdapter(peers); + + RecyclerView peersRecyclerView = binding.peersRecyclerView; + peersRecyclerView.setAdapter(adapter); + peersRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); + + model.getUiState().observe(getViewLifecycleOwner(), uiState -> { + peers.clear(); + peers.addAll(uiState.getPeers()); + + updatePeersCounter(peers); - List peerList = peersInfoToPeersList(peersInfo); - updatePeerCount(peersInfo); - peersListView = binding.peersRecyclerView; - peersListView.setLayoutManager(new LinearLayoutManager(requireContext())); - PeersAdapter adapter = new PeersAdapter(peerList); - peersListView.setAdapter(adapter); + ZeroPeerView.updateVisibility(binding.zeroPeerLayout, binding.peersList, !peers.isEmpty()); + adapter.notifyDataSetChanged(); + adapter.filterBySearchQuery(binding.searchView.getText().toString()); + }); binding.searchView.clearFocus(); binding.searchView.addTextChangedListener(new TextWatcher() { @@ -127,37 +143,33 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat @Override public void onDetach() { - super.onDetach(); + stateListenerRegistry = null; serviceAccessor = null; + + super.onDetach(); } @Override public void onDestroyView() { - super.onDestroyView(); + if (model != null) { + stateListenerRegistry.unregisterServiceStateListener(model.getStateListener()); + } binding = null; + super.onDestroyView(); } - private List peersInfoToPeersList(PeerInfoArray peersInfo) { - List peerList = new ArrayList<>(); - for (int i = 0; i < peersInfo.size(); i++) { - PeerInfo peerInfo = peersInfo.get(i); - Status status = Status.fromString(peerInfo.getConnStatus()); - peerList.add(new Peer(status, peerInfo.getIP(), peerInfo.getFQDN())); - } - return peerList; - } + private void updatePeersCounter(List peers) { + TextView textPeersCount = binding.textOpenPanel; - private void updatePeerCount(PeerInfoArray peersInfo) { int connected = 0; - for (int i = 0; i < peersInfo.size(); i++) { - PeerInfo peer = peersInfo.get(i); - if (peer.getConnStatus().equalsIgnoreCase(Status.CONNECTED.toString())) { + + for (var peer : peers) { + if (peer.getStatus() == Status.CONNECTED) { connected++; } } - TextView textPeersCount = binding.textOpenPanel; - String text = getString(R.string.peers_connected, connected, peersInfo.size()); + String text = getString(R.string.peers_connected, connected, peers.size()); textPeersCount.post(() -> textPeersCount.setText(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY)) ); diff --git a/app/src/main/java/io/netbird/client/ui/home/PeersFragmentUiState.java b/app/src/main/java/io/netbird/client/ui/home/PeersFragmentUiState.java new file mode 100644 index 00000000..be797baa --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/home/PeersFragmentUiState.java @@ -0,0 +1,17 @@ +package io.netbird.client.ui.home; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PeersFragmentUiState { + private final List peers; + + public PeersFragmentUiState(List peers) { + this.peers = new ArrayList<>(peers); + } + + public List getPeers() { + return Collections.unmodifiableList(peers); + } +} diff --git a/app/src/main/java/io/netbird/client/ui/home/PeersFragmentViewModel.java b/app/src/main/java/io/netbird/client/ui/home/PeersFragmentViewModel.java new file mode 100644 index 00000000..4175c901 --- /dev/null +++ b/app/src/main/java/io/netbird/client/ui/home/PeersFragmentViewModel.java @@ -0,0 +1,88 @@ +package io.netbird.client.ui.home; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import java.util.ArrayList; +import java.util.List; + +import io.netbird.client.PeersStateListener; +import io.netbird.client.PeersStateListenerAdapter; +import io.netbird.client.ServiceAccessor; +import io.netbird.client.StateListener; +import io.netbird.gomobile.android.PeerInfo; +import io.netbird.gomobile.android.PeerInfoArray; + +public class PeersFragmentViewModel extends ViewModel implements PeersStateListener { + private final PeersStateListenerAdapter peersAdapter; + private final ServiceAccessor serviceAccessor; + + private final MutableLiveData uiState = + new MutableLiveData<>(new PeersFragmentUiState(new ArrayList<>())); + + public PeersFragmentViewModel(ServiceAccessor serviceAccessor) { + peersAdapter = new PeersStateListenerAdapter(this); + this.serviceAccessor = serviceAccessor; + } + + public static ViewModelProvider.Factory getFactory(ServiceAccessor serviceAccessor) { + return new ViewModelProvider.Factory() { + @NonNull + @Override + @SuppressWarnings("unchecked") + public T create(@NonNull Class modelClass) { + if (modelClass.isAssignableFrom(PeersFragmentViewModel.class)) { + return (T) new PeersFragmentViewModel(serviceAccessor); + } + throw new IllegalArgumentException("Unknown ViewModel class"); + } + }; + } + + private List getPeers(PeerInfoArray peersInfo) { + List peers = new ArrayList<>(); + PeerInfo peerInfo; + String connStatus; + Status status; + + for (int i = 0; i < peersInfo.size(); i++) { + peerInfo = peersInfo.get(i); + + if (peerInfo == null) { + continue; + } + + connStatus = peerInfo.getConnStatus(); + if (connStatus == null) { + continue; + } + + status = Status.fromString(connStatus); + peers.add(new Peer(status, peerInfo.getIP(), peerInfo.getFQDN())); + } + return peers; + } + + public LiveData getUiState() { + return uiState; + } + + public StateListener getStateListener() { + return this.peersAdapter; + } + + @Override + protected void onCleared() { + peersAdapter.clearListener(); + super.onCleared(); + } + + @Override + public void onPeersChanged(long totalPeers) { + var peers = getPeers(serviceAccessor.getPeersList()); + this.uiState.postValue(new PeersFragmentUiState(peers)); + } +} diff --git a/app/src/main/java/io/netbird/client/ui/home/Status.java b/app/src/main/java/io/netbird/client/ui/home/Status.java index 69fe50ed..3d3d0d75 100644 --- a/app/src/main/java/io/netbird/client/ui/home/Status.java +++ b/app/src/main/java/io/netbird/client/ui/home/Status.java @@ -1,6 +1,8 @@ package io.netbird.client.ui.home; +import java.util.Locale; + public enum Status { IDLE, CONNECTING, @@ -25,7 +27,7 @@ public static Status fromString(String status) { throw new IllegalArgumentException("Status string cannot be null"); } - switch (status.toLowerCase()) { + switch (status.toLowerCase(Locale.ROOT)) { case "idle": return IDLE; case "connecting": @@ -33,7 +35,7 @@ public static Status fromString(String status) { case "connected": return CONNECTED; default: - throw new IllegalArgumentException("Unknown status: " + status); + throw new IllegalArgumentException("Unknown status: " + status.toLowerCase(Locale.ROOT)); } } }