diff --git a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt index 65a684477f..fb2eb8c20a 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt @@ -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, diff --git a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt index fa92360b78..b139b46098 100644 --- a/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt +++ b/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt @@ -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 = _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? = 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 + } } }