You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tailscale-android/android/src/main/java/com/tailscale/ipn/ui/viewModel/MainViewModel.kt

150 lines
4.9 KiB
Kotlin

// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn.ui.viewModel
import android.content.Intent
import android.net.VpnService
import android.util.Log
import androidx.activity.result.ActivityResultLauncher
import androidx.lifecycle.viewModelScope
import com.tailscale.ipn.App
import com.tailscale.ipn.R
import com.tailscale.ipn.mdm.MDMSettings
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Ipn.State
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.util.PeerCategorizer
import com.tailscale.ipn.ui.util.PeerSet
import com.tailscale.ipn.ui.util.TimeUtil
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.launch
import java.time.Duration
class MainViewModel : IpnViewModel() {
// The user readable state of the system
val stateRes: StateFlow<Int> = MutableStateFlow(userStringRes(State.NoState, State.NoState, true))
// The expected state of the VPN toggle
private val _vpnToggleState = MutableStateFlow(false)
val vpnToggleState: StateFlow<Boolean> = _vpnToggleState
// Permission to prepare VPN
private var vpnPermissionLauncher: ActivityResultLauncher<Intent>? = null
// The list of peers
val peers: StateFlow<List<PeerSet>> = MutableStateFlow(emptyList<PeerSet>())
// The current state of the IPN for determining view visibility
val ipnState = Notifier.state
val prefs = Notifier.prefs
val netmap = Notifier.netmap
// The active search term for filtering peers
val searchTerm: StateFlow<String> = MutableStateFlow("")
// True if we should render the key expiry bannder
val showExpiry: StateFlow<Boolean> = MutableStateFlow(false)
private val peerCategorizer = PeerCategorizer()
init {
viewModelScope.launch {
var previousState: State? = null
combine(Notifier.state, vpnPrepared) { state, prepared -> state to prepared }
.collect { (currentState, prepared) ->
stateRes.set(userStringRes(currentState, previousState, prepared))
val isOn =
when {
currentState == State.Running || currentState == State.Starting -> true
previousState == State.NoState && currentState == State.Starting -> true
else -> false
}
_vpnToggleState.value = isOn
previousState = currentState
}
}
viewModelScope.launch {
Notifier.netmap.collect { it ->
it?.let { netmap ->
peerCategorizer.regenerateGroupedPeers(netmap)
peers.set(peerCategorizer.groupedAndFilteredPeers(searchTerm.value))
if (netmap.SelfNode.keyDoesNotExpire) {
showExpiry.set(false)
return@let
} else {
val expiryNotificationWindowMDM = MDMSettings.keyExpirationNotice.flow.value
val window =
expiryNotificationWindowMDM?.let { TimeUtil.duration(it) } ?: Duration.ofHours(24)
val expiresSoon =
TimeUtil.isWithinExpiryNotificationWindow(window, it.SelfNode.KeyExpiry)
showExpiry.set(expiresSoon)
}
}
}
}
viewModelScope.launch {
searchTerm.collect { term -> peers.set(peerCategorizer.groupedAndFilteredPeers(term)) }
}
viewModelScope.launch {
Notifier.prefs.collect { prefs -> Log.d(TAG, "Main VM - prefs = ${prefs}") }
}
}
fun showVPNPermissionLauncherIfUnauthorized() {
val vpnIntent = VpnService.prepare(App.get())
if (vpnIntent != null) {
vpnPermissionLauncher?.launch(vpnIntent)
} else {
setVpnPrepared(true)
}
}
fun toggleVpn() {
val state = Notifier.state.value
val isPrepared = vpnPrepared.value
when {
!isPrepared -> showVPNPermissionLauncherIfUnauthorized()
state == Ipn.State.Running -> stopVPN()
else -> startVPN()
}
}
fun searchPeers(searchTerm: String) {
this.searchTerm.set(searchTerm)
}
fun setVpnPermissionLauncher(launcher: ActivityResultLauncher<Intent>) {
// No intent means we're already authorized
vpnPermissionLauncher = launcher
}
}
private fun userStringRes(currentState: State?, previousState: State?, vpnPrepared: Boolean): Int {
return when {
previousState == State.NoState && currentState == State.Starting -> R.string.starting
currentState == State.NoState -> R.string.placeholder
currentState == State.InUseOtherUser -> R.string.placeholder
currentState == State.NeedsLogin ->
if (vpnPrepared) R.string.please_login else R.string.connect_to_vpn
currentState == State.NeedsMachineAuth -> R.string.needs_machine_auth
currentState == State.Stopped -> R.string.stopped
currentState == State.Starting -> R.string.starting
currentState == State.Running -> R.string.connected
else -> R.string.placeholder
}
}