android: synchronize ipn state and UI (#585)

Pass in intended state to toggleVpn and keep track of progress to avoid redundant updates and ensure that the action taken reflects the user's intent.
This fixes a possible recomposition loop caused by the ipn state and the vpn toggle state getting out of sync.

Updates tailscale/tailscale#14125

Signed-off-by: kari-ts <kari@tailscale.com>
pull/587/head
kari-ts 12 months ago committed by GitHub
parent 45ddef1a90
commit db6f9fe5c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -145,13 +145,12 @@ fun MainView(
leadingContent = {
if (!hideHeader) {
TintedSwitch(
onCheckedChange = {
if (!disableToggle.value) {
viewModel.toggleVpn()
}
},
enabled = !disableToggle.value,
checked = isOn)
checked = isOn,
enabled =
!disableToggle.value &&
!viewModel.isToggleInProgress
.value, // Disable switch if toggle is in progress
onCheckedChange = { desiredState -> viewModel.toggleVpn(desiredState) })
}
},
headlineContent = {
@ -228,7 +227,7 @@ fun MainView(
// action (eg, if the user connected to another VPN).
state != Ipn.State.Stopping,
user,
{ viewModel.toggleVpn() },
{ viewModel.toggleVpn(desiredState = !isOn) },
{ viewModel.login() },
loginAtUrl,
netmap?.SelfNode,

@ -18,7 +18,6 @@ import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn.State
import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV
import com.tailscale.ipn.ui.util.PeerCategorizer
import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.TimeUtil
@ -53,6 +52,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
private val _vpnToggleState = MutableStateFlow(false)
val vpnToggleState: StateFlow<Boolean> = _vpnToggleState
// Keeps track of whether a toggle operation is in progress. This ensures that toggleVpn cannot be
// invoked until the current operation is complete.
var isToggleInProgress = MutableStateFlow(false)
// Permission to prepare VPN
private var vpnPermissionLauncher: ActivityResultLauncher<Intent>? = null
@ -184,15 +187,33 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
}
}
fun toggleVpn() {
val state = Notifier.state.value
val isPrepared = vpnViewModel.vpnPrepared.value
fun toggleVpn(desiredState: Boolean) {
if (isToggleInProgress.value) {
// Prevent toggling while a previous toggle is in progress
return
}
when {
!isPrepared -> showVPNPermissionLauncherIfUnauthorized()
state == Ipn.State.Running -> stopVPN()
state == Ipn.State.NeedsLogin && isAndroidTV() -> login()
else -> startVPN()
viewModelScope.launch {
isToggleInProgress.value = true
try {
val currentState = Notifier.state.value
val isPrepared = vpnViewModel.vpnPrepared.value
if (desiredState) {
// User wants to turn ON the VPN
when {
!isPrepared -> showVPNPermissionLauncherIfUnauthorized()
currentState != Ipn.State.Running -> startVPN()
}
} else {
// User wants to turn OFF the VPN
if (currentState == Ipn.State.Running) {
stopVPN()
}
}
} finally {
isToggleInProgress.value = false
}
}
}

Loading…
Cancel
Save