android/src/main: show exit node information in the permanent notification

Displays exit node status (including the name of the exit node) in the permanent connection notification's content (moving the overall connected/disconnected status to the title).

Fixes tailscale/tailscale#14438

Signed-off-by: Jakub Meysner <git@jakubmeysner.com>
pull/642/head
Jakub Meysner 8 months ago
parent 74c4274161
commit 8a43a75209
No known key found for this signature in database

@ -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)

@ -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,

@ -9,6 +9,7 @@
<string name="disconnect">Disconnect</string>
<string name="unknown_user">Unknown user</string>
<string name="connected">Connected</string>
<string name="using_exit_node">Using exit node (%s)</string>
<string name="not_connected">Not connected</string>
<string name="empty" translatable="false"> </string>
<string name="template" translatable="false">%s</string>

Loading…
Cancel
Save