diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt
index 041bdd9..a91325e 100644
--- a/android/src/main/java/com/tailscale/ipn/App.kt
+++ b/android/src/main/java/com/tailscale/ipn/App.kt
@@ -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)
diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt
index 120e469..917b405 100644
--- a/android/src/main/java/com/tailscale/ipn/IPNService.kt
+++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt
@@ -47,11 +47,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
START_NOT_STICKY
}
ACTION_START_VPN -> {
- scope.launch {
- // Collect the first value of hideDisconnectAction asynchronously.
- val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
- showForegroundNotification(hideDisconnectAction.value)
- }
+ scope.launch { showForegroundNotification() }
app.setWantRunning(true)
Libtailscale.requestVPN(this)
START_STICKY
@@ -63,7 +59,9 @@ open class IPNService : VpnService(), libtailscale.IPNService {
scope.launch {
// Collect the first value of hideDisconnectAction asynchronously.
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
- app.notifyStatus(true, hideDisconnectAction.value)
+ val exitNodeName =
+ UninitializedApp.getExitNodeName(Notifier.prefs.value, Notifier.netmap.value)
+ app.notifyStatus(true, hideDisconnectAction.value, exitNodeName)
}
app.setWantRunning(true)
Libtailscale.requestVPN(this)
@@ -73,11 +71,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
// This means that we were restarted after the service was killed
// (potentially due to OOM).
if (UninitializedApp.get().isAbleToStartVPN()) {
- scope.launch {
- // Collect the first value of hideDisconnectAction asynchronously.
- val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
- showForegroundNotification(hideDisconnectAction.value)
- }
+ scope.launch { showForegroundNotification() }
App.get()
Libtailscale.requestVPN(this)
START_STICKY
@@ -114,16 +108,25 @@ open class IPNService : VpnService(), libtailscale.IPNService {
app.getAppScopedViewModel().setVpnPrepared(isPrepared)
}
- private fun showForegroundNotification(hideDisconnectAction: Boolean) {
+ private fun showForegroundNotification(
+ hideDisconnectAction: Boolean,
+ exitNodeName: String? = null
+ ) {
try {
startForeground(
UninitializedApp.STATUS_NOTIFICATION_ID,
- UninitializedApp.get().buildStatusNotification(true, hideDisconnectAction))
+ UninitializedApp.get().buildStatusNotification(true, hideDisconnectAction, exitNodeName))
} catch (e: Exception) {
TSLog.e(TAG, "Failed to start foreground service: $e")
}
}
+ private fun showForegroundNotification() {
+ val hideDisconnectAction = MDMSettings.forceEnabled.flow.value.value
+ val exitNodeName = UninitializedApp.getExitNodeName(Notifier.prefs.value, Notifier.netmap.value)
+ showForegroundNotification(hideDisconnectAction, exitNodeName)
+ }
+
private fun configIntent(): PendingIntent {
return PendingIntent.getActivity(
this,
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
index c1c539a..5626e58 100644
--- a/android/src/main/res/values/strings.xml
+++ b/android/src/main/res/values/strings.xml
@@ -9,6 +9,7 @@
Disconnect
Unknown user
Connected
+ Using exit node (%s)
Not connected
%s