android: make ExitNodePickerViewModel reactive

Updates tailscale/corp#18202

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

@ -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

@ -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<Boolean> = 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 })
}
}
}
}
}

Loading…
Cancel
Save