@ -30,6 +30,7 @@ import com.tailscale.ipn.mdm.MDMSettingsChangedReceiver
import com.tailscale.ipn.ui.localapi.Client
import com.tailscale.ipn.ui.localapi.Request
import com.tailscale.ipn.ui.model.Ipn
import com.tailscale.ipn.ui.model.Netmap
import com.tailscale.ipn.ui.notifier.HealthNotifier
import com.tailscale.ipn.ui.notifier.Notifier
import com.tailscale.ipn.ui.viewModel.VpnViewModel
@ -46,6 +47,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
@ -165,10 +167,15 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
initViewModels ( )
applicationScope . launch {
Notifier . state . collect { _ ->
combine ( Notifier . state , MDMSettings . forceEnabled . flow ) { state , forceEnabled ->
Pair ( state , forceEnabled )
combine ( Notifier . state , MDMSettings . forceEnabled . flow , Notifier . prefs , Notifier . netmap ) {
state ,
forceEnabled ,
prefs ,
netmap ->
Triple ( state , forceEnabled , getExitNodeName ( prefs , netmap ) )
}
. collect { ( state , hideDisconnectAction ) ->
. distinctUntilChanged ( )
. collect { ( state , hideDisconnectAction , exitNodeName ) ->
val ableToStartVPN = state > Ipn . State . NeedsMachineAuth
// If VPN is stopped, show a disconnected notification. If it is running as a
// foreground
@ -183,7 +190,10 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
// Update notification status when VPN is running
if ( vpnRunning ) {
notifyStatus ( vpnRunning = true , hideDisconnectAction = hideDisconnectAction . value )
notifyStatus (
vpnRunning = true ,
hideDisconnectAction = hideDisconnectAction . value ,
exitNodeName = exitNodeName )
}
}
}
@ -391,6 +401,18 @@ open class UninitializedApp : Application() {
fun get ( ) : UninitializedApp {
return appInstance
}
/ * *
* Return the name of the active ( but not the selected / prior one ) exit node based on the
* provided [ Ipn . Prefs ] and [ Netmap . NetworkMap ] .
*
* @return The name of the exit node or `null` if there isn ' t one .
* /
fun getExitNodeName ( prefs : Ipn . Prefs ? , netmap : Netmap . NetworkMap ? ) : String ? {
return prefs ?. activeExitNodeID ?. let { exitNodeID ->
netmap ?. Peers ?. find { it . StableID == exitNodeID } ?. exitNodeName
}
}
}
protected fun setUnprotectedInstance ( instance : UninitializedApp ) {
@ -476,8 +498,12 @@ open class UninitializedApp : Application() {
notificationManager . createNotificationChannel ( channel )
}
fun notifyStatus ( vpnRunning : Boolean , hideDisconnectAction : Boolean ) {
notifyStatus ( buildStatusNotification ( vpnRunning , hideDisconnectAction ) )
fun notifyStatus (
vpnRunning : Boolean ,
hideDisconnectAction : Boolean ,
exitNodeName : String ? = null
) {
notifyStatus ( buildStatusNotification ( vpnRunning , hideDisconnectAction , exitNodeName ) )
}
fun notifyStatus ( notification : Notification ) {
@ -495,8 +521,16 @@ open class UninitializedApp : Application() {
notificationManager . notify ( STATUS _NOTIFICATION _ID , notification )
}
fun buildStatusNotification ( vpnRunning : Boolean , hideDisconnectAction : Boolean ) : Notification {
val message = getString ( if ( vpnRunning ) R . string . connected else R . string . not _connected )
fun buildStatusNotification (
vpnRunning : Boolean ,
hideDisconnectAction : Boolean ,
exitNodeName : String ? = null
) : Notification {
val title = getString ( if ( vpnRunning ) R . string . connected else R . string . not _connected )
val message =
if ( vpnRunning && exitNodeName != null ) {
getString ( R . string . using _exit _node , exitNodeName )
} else null
val icon = if ( vpnRunning ) R . drawable . ic _notification else R . drawable . ic _notification _disabled
val action =
if ( vpnRunning ) IPNReceiver . INTENT _DISCONNECT _VPN else IPNReceiver . INTENT _CONNECT _VPN
@ -520,7 +554,7 @@ open class UninitializedApp : Application() {
val builder =
NotificationCompat . Builder ( this , STATUS _CHANNEL _ID )
. setSmallIcon ( icon )
. setContentTitle ( getString( R . string . app _name ) )
. setContentTitle ( title )
. setContentText ( message )
. setAutoCancel ( ! vpnRunning )
. setOnlyAlertOnce ( ! vpnRunning )