android: make ExitNodePickerViewModel reactive

Updates tailscale/corp#18202

Signed-off-by: Percy Wegmann <percy@tailscale.com>
pull/209/head
Percy Wegmann 3 months ago committed by Percy Wegmann
parent a1e67ff1e9
commit e568741081

@ -75,6 +75,10 @@ class Tailcfg {
) { ) {
val isAdmin: Boolean val isAdmin: Boolean
get() = (Capabilities ?: emptyList()).contains("https://tailscale.com/cap/is-admin") 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 @Serializable

@ -4,17 +4,20 @@
package com.tailscale.ipn.ui.viewModel package com.tailscale.ipn.ui.viewModel
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.ui.localapi.Client import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.StableNodeID 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.LoadingIndicator
import com.tailscale.ipn.ui.util.set import com.tailscale.ipn.ui.util.set
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import java.util.TreeMap import java.util.TreeMap
data class ExitNodePickerNav( data class ExitNodePickerNav(
@ -52,66 +55,66 @@ class ExitNodePickerViewModel(private val nav: ExitNodePickerNav) : IpnViewModel
val anyActive: StateFlow<Boolean> = MutableStateFlow(false) val anyActive: StateFlow<Boolean> = MutableStateFlow(false)
init { init {
Client(viewModelScope).status { result -> viewModelScope.launch {
result.onFailure { Notifier.netmap.combine(Notifier.prefs) { netmap, prefs -> Pair(netmap, prefs) }
Log.e(TAG, "getStatus: ${it.message}") .stateIn(viewModelScope).collect { (netmap, prefs) ->
}.onSuccess { val exitNodeId = prefs?.ExitNodeID
it.Peer?.values?.let { peers -> netmap?.Peers?.let { peers ->
val allNodes = peers.filter { it.ExitNodeOption }.map { val allNodes = peers.filter { it.isExitNode }.map {
ExitNode( ExitNode(
id = it.ID, id = it.StableID,
label = it.DNSName, label = it.Name,
online = it.Online, online = it.Online ?: false,
selected = it.ExitNode, selected = it.StableID == exitNodeId,
mullvad = it.DNSName.endsWith(".mullvad.ts.net."), mullvad = it.Name.endsWith(".mullvad.ts.net."),
priority = it.Location?.Priority ?: 0, priority = it.Hostinfo?.Location?.Priority ?: 0,
countryCode = it.Location?.CountryCode ?: "", countryCode = it.Hostinfo?.Location?.CountryCode ?: "",
country = it.Location?.Country ?: "", country = it.Hostinfo?.Location?.Country ?: "",
city = it.Location?.City ?: "", city = it.Hostinfo?.Location?.City ?: "",
) )
} }
val tailnetNodes = allNodes.filter { !it.mullvad } val tailnetNodes = allNodes.filter { !it.mullvad }
tailnetExitNodes.set(tailnetNodes.sortedWith { a, b -> tailnetExitNodes.set(tailnetNodes.sortedWith { a, b ->
a.label.compareTo( a.label.compareTo(
b.label b.label
) )
}) })
val mullvadExitNodes = allNodes.filter { val mullvadExitNodes = allNodes.filter {
// Pick all mullvad nodes that are online or the currently selected // Pick all mullvad nodes that are online or the currently selected
it.mullvad && (it.selected || it.online) it.mullvad && (it.selected || it.online)
}.groupBy { }.groupBy {
// Group by countryCode // Group by countryCode
it.countryCode it.countryCode
}.mapValues { (_, nodes) ->
// Group by city
nodes.groupBy {
it.city
}.mapValues { (_, nodes) -> }.mapValues { (_, nodes) ->
// Pick one node per city, either the selected one or the best // Group by city
// available nodes.groupBy {
nodes.sortedWith { a, b -> it.city
if (a.selected && !b.selected) { }.mapValues { (_, nodes) ->
-1 // Pick one node per city, either the selected one or the best
} else if (b.selected && !a.selected) { // available
1 nodes.sortedWith { a, b ->
} else { if (a.selected && !b.selected) {
b.priority.compareTo(a.priority) -1
} } else if (b.selected && !a.selected) {
}.first() 1
}.values.sortedBy { it.city.lowercase() } } else {
} b.priority.compareTo(a.priority)
mullvadExitNodesByCountryCode.set(mullvadExitNodes) }
}.first()
}.values.sortedBy { it.city.lowercase() }
}
mullvadExitNodesByCountryCode.set(mullvadExitNodes)
val bestAvailableByCountry = mullvadExitNodes.mapValues { (_, nodes) -> val bestAvailableByCountry = mullvadExitNodes.mapValues { (_, nodes) ->
nodes.minByOrNull { -1 * it.priority }!! nodes.minByOrNull { -1 * it.priority }!!
} }
mullvadBestAvailableByCountry.set(bestAvailableByCountry) mullvadBestAvailableByCountry.set(bestAvailableByCountry)
anyActive.set(allNodes.any { it.selected }) anyActive.set(allNodes.any { it.selected })
}
} }
}
} }
} }

Loading…
Cancel
Save