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 = { leadingContent = {
if (!hideHeader) { if (!hideHeader) {
TintedSwitch( TintedSwitch(
onCheckedChange = { checked = isOn,
if (!disableToggle.value) { enabled =
viewModel.toggleVpn() !disableToggle.value &&
} !viewModel.isToggleInProgress
}, .value, // Disable switch if toggle is in progress
enabled = !disableToggle.value, onCheckedChange = { desiredState -> viewModel.toggleVpn(desiredState) })
checked = isOn)
} }
}, },
headlineContent = { headlineContent = {
@ -228,7 +227,7 @@ fun MainView(
// action (eg, if the user connected to another VPN). // action (eg, if the user connected to another VPN).
state != Ipn.State.Stopping, state != Ipn.State.Stopping,
user, user,
{ viewModel.toggleVpn() }, { viewModel.toggleVpn(desiredState = !isOn) },
{ viewModel.login() }, { viewModel.login() },
loginAtUrl, loginAtUrl,
netmap?.SelfNode, 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.Ipn.State
import com.tailscale.ipn.ui.model.Tailcfg import com.tailscale.ipn.ui.model.Tailcfg
import com.tailscale.ipn.ui.notifier.Notifier 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.PeerCategorizer
import com.tailscale.ipn.ui.util.PeerSet import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.TimeUtil import com.tailscale.ipn.ui.util.TimeUtil
@ -53,6 +52,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
private val _vpnToggleState = MutableStateFlow(false) private val _vpnToggleState = MutableStateFlow(false)
val vpnToggleState: StateFlow<Boolean> = _vpnToggleState 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 // Permission to prepare VPN
private var vpnPermissionLauncher: ActivityResultLauncher<Intent>? = null private var vpnPermissionLauncher: ActivityResultLauncher<Intent>? = null
@ -184,15 +187,33 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
} }
} }
fun toggleVpn() { fun toggleVpn(desiredState: Boolean) {
val state = Notifier.state.value if (isToggleInProgress.value) {
val isPrepared = vpnViewModel.vpnPrepared.value // Prevent toggling while a previous toggle is in progress
return
}
when { viewModelScope.launch {
!isPrepared -> showVPNPermissionLauncherIfUnauthorized() isToggleInProgress.value = true
state == Ipn.State.Running -> stopVPN() try {
state == Ipn.State.NeedsLogin && isAndroidTV() -> login() val currentState = Notifier.state.value
else -> startVPN() 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