Skip to content

Commit db6f9fe

Browse files
authored
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 <[email protected]>
1 parent 45ddef1 commit db6f9fe

File tree

2 files changed

+37
-17
lines changed

2 files changed

+37
-17
lines changed

android/src/main/java/com/tailscale/ipn/ui/view/MainView.kt

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,12 @@ fun MainView(
145145
leadingContent = {
146146
if (!hideHeader) {
147147
TintedSwitch(
148-
onCheckedChange = {
149-
if (!disableToggle.value) {
150-
viewModel.toggleVpn()
151-
}
152-
},
153-
enabled = !disableToggle.value,
154-
checked = isOn)
148+
checked = isOn,
149+
enabled =
150+
!disableToggle.value &&
151+
!viewModel.isToggleInProgress
152+
.value, // Disable switch if toggle is in progress
153+
onCheckedChange = { desiredState -> viewModel.toggleVpn(desiredState) })
155154
}
156155
},
157156
headlineContent = {
@@ -228,7 +227,7 @@ fun MainView(
228227
// action (eg, if the user connected to another VPN).
229228
state != Ipn.State.Stopping,
230229
user,
231-
{ viewModel.toggleVpn() },
230+
{ viewModel.toggleVpn(desiredState = !isOn) },
232231
{ viewModel.login() },
233232
loginAtUrl,
234233
netmap?.SelfNode,

android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import com.tailscale.ipn.ui.model.Ipn
1818
import com.tailscale.ipn.ui.model.Ipn.State
1919
import com.tailscale.ipn.ui.model.Tailcfg
2020
import com.tailscale.ipn.ui.notifier.Notifier
21-
import com.tailscale.ipn.ui.util.AndroidTVUtil.isAndroidTV
2221
import com.tailscale.ipn.ui.util.PeerCategorizer
2322
import com.tailscale.ipn.ui.util.PeerSet
2423
import com.tailscale.ipn.ui.util.TimeUtil
@@ -53,6 +52,10 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
5352
private val _vpnToggleState = MutableStateFlow(false)
5453
val vpnToggleState: StateFlow<Boolean> = _vpnToggleState
5554

55+
// Keeps track of whether a toggle operation is in progress. This ensures that toggleVpn cannot be
56+
// invoked until the current operation is complete.
57+
var isToggleInProgress = MutableStateFlow(false)
58+
5659
// Permission to prepare VPN
5760
private var vpnPermissionLauncher: ActivityResultLauncher<Intent>? = null
5861

@@ -184,15 +187,33 @@ class MainViewModel(private val vpnViewModel: VpnViewModel) : IpnViewModel() {
184187
}
185188
}
186189

187-
fun toggleVpn() {
188-
val state = Notifier.state.value
189-
val isPrepared = vpnViewModel.vpnPrepared.value
190+
fun toggleVpn(desiredState: Boolean) {
191+
if (isToggleInProgress.value) {
192+
// Prevent toggling while a previous toggle is in progress
193+
return
194+
}
190195

191-
when {
192-
!isPrepared -> showVPNPermissionLauncherIfUnauthorized()
193-
state == Ipn.State.Running -> stopVPN()
194-
state == Ipn.State.NeedsLogin && isAndroidTV() -> login()
195-
else -> startVPN()
196+
viewModelScope.launch {
197+
isToggleInProgress.value = true
198+
try {
199+
val currentState = Notifier.state.value
200+
val isPrepared = vpnViewModel.vpnPrepared.value
201+
202+
if (desiredState) {
203+
// User wants to turn ON the VPN
204+
when {
205+
!isPrepared -> showVPNPermissionLauncherIfUnauthorized()
206+
currentState != Ipn.State.Running -> startVPN()
207+
}
208+
} else {
209+
// User wants to turn OFF the VPN
210+
if (currentState == Ipn.State.Running) {
211+
stopVPN()
212+
}
213+
}
214+
} finally {
215+
isToggleInProgress.value = false
216+
}
196217
}
197218
}
198219

0 commit comments

Comments
 (0)