From e568741081c1c1a7bc1dc12df55e430264ef2d7a Mon Sep 17 00:00:00 2001 From: Percy Wegmann Date: Mon, 18 Mar 2024 14:00:11 -0500 Subject: [PATCH] android: make ExitNodePickerViewModel reactive Updates tailscale/corp#18202 Signed-off-by: Percy Wegmann --- .../com/tailscale/ipn/ui/model/TailCfg.kt | 4 + .../ui/viewModel/ExitNodePickerViewModel.kt | 113 +++++++++--------- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt index dbab647..baa417f 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/model/TailCfg.kt @@ -75,6 +75,10 @@ class Tailcfg { ) { val isAdmin: Boolean get() = (Capabilities ?: emptyList()).contains("https://tailscale.com/cap/is-admin") + + // isExitNode reproduces the Go logic in local.go peerStatusFromNode + val isExitNode: Boolean = + AllowedIPs?.contains("0.0.0.0/0") ?: false && AllowedIPs?.contains("::/0") ?: false } @Serializable diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/ExitNodePickerViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/ExitNodePickerViewModel.kt index b71b213..8dfffd4 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/ExitNodePickerViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/ExitNodePickerViewModel.kt @@ -4,17 +4,20 @@ package com.tailscale.ipn.ui.viewModel -import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.StableNodeID +import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.ui.util.LoadingIndicator import com.tailscale.ipn.ui.util.set import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import java.util.TreeMap data class ExitNodePickerNav( @@ -52,66 +55,66 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel val anyActive: StateFlow = MutableStateFlow(false) init { - Client(viewModelScope).status { result -> - result.onFailure { - Log.e(TAG, "getStatus: ${it.message}") - }.onSuccess { - it.Peer?.values?.let { peers -> - val allNodes = peers.filter { it.ExitNodeOption }.map { - ExitNode( - id = it.ID, - label = it.DNSName, - online = it.Online, - selected = it.ExitNode, - mullvad = it.DNSName.endsWith(".mullvad.ts.net."), - priority = it.Location?.Priority ?: 0, - countryCode = it.Location?.CountryCode ?: "", - country = it.Location?.Country ?: "", - city = it.Location?.City ?: "", - ) - } + viewModelScope.launch { + Notifier.netmap.combine(Notifier.prefs) { netmap, prefs -> Pair(netmap, prefs) } + .stateIn(viewModelScope).collect { (netmap, prefs) -> + val exitNodeId = prefs?.ExitNodeID + netmap?.Peers?.let { peers -> + val allNodes = peers.filter { it.isExitNode }.map { + ExitNode( + id = it.StableID, + label = it.Name, + online = it.Online ?: false, + selected = it.StableID == exitNodeId, + mullvad = it.Name.endsWith(".mullvad.ts.net."), + priority = it.Hostinfo?.Location?.Priority ?: 0, + countryCode = it.Hostinfo?.Location?.CountryCode ?: "", + country = it.Hostinfo?.Location?.Country ?: "", + city = it.Hostinfo?.Location?.City ?: "", + ) + } - val tailnetNodes = allNodes.filter { !it.mullvad } - tailnetExitNodes.set(tailnetNodes.sortedWith { a, b -> - a.label.compareTo( - b.label - ) - }) + val tailnetNodes = allNodes.filter { !it.mullvad } + tailnetExitNodes.set(tailnetNodes.sortedWith { a, b -> + a.label.compareTo( + b.label + ) + }) - val mullvadExitNodes = allNodes.filter { - // Pick all mullvad nodes that are online or the currently selected - it.mullvad && (it.selected || it.online) - }.groupBy { - // Group by countryCode - it.countryCode - }.mapValues { (_, nodes) -> - // Group by city - nodes.groupBy { - it.city + val mullvadExitNodes = allNodes.filter { + // Pick all mullvad nodes that are online or the currently selected + it.mullvad && (it.selected || it.online) + }.groupBy { + // Group by countryCode + it.countryCode }.mapValues { (_, nodes) -> - // Pick one node per city, either the selected one or the best - // available - nodes.sortedWith { a, b -> - if (a.selected && !b.selected) { - -1 - } else if (b.selected && !a.selected) { - 1 - } else { - b.priority.compareTo(a.priority) - } - }.first() - }.values.sortedBy { it.city.lowercase() } - } - mullvadExitNodesByCountryCode.set(mullvadExitNodes) + // Group by city + nodes.groupBy { + it.city + }.mapValues { (_, nodes) -> + // Pick one node per city, either the selected one or the best + // available + nodes.sortedWith { a, b -> + if (a.selected && !b.selected) { + -1 + } else if (b.selected && !a.selected) { + 1 + } else { + b.priority.compareTo(a.priority) + } + }.first() + }.values.sortedBy { it.city.lowercase() } + } + mullvadExitNodesByCountryCode.set(mullvadExitNodes) - val bestAvailableByCountry = mullvadExitNodes.mapValues { (_, nodes) -> - nodes.minByOrNull { -1 * it.priority }!! - } - mullvadBestAvailableByCountry.set(bestAvailableByCountry) + val bestAvailableByCountry = mullvadExitNodes.mapValues { (_, nodes) -> + nodes.minByOrNull { -1 * it.priority }!! + } + mullvadBestAvailableByCountry.set(bestAvailableByCountry) - anyActive.set(allNodes.any { it.selected }) + anyActive.set(allNodes.any { it.selected }) + } } - } } }