From 7c617fb32cf7afbbc1fc142bc1a057217a028d6a Mon Sep 17 00:00:00 2001 From: kari-ts Date: Wed, 9 Oct 2024 13:24:36 -0700 Subject: [PATCH] android: hide disconnect action if force enabled In notification, don't show 'Disconnect' button if MDM force enable is on. Fixes tailscale/corp#23764 Signed-off-by: kari-ts --- .../src/main/java/com/tailscale/ipn/App.kt | 70 ++++++++++++------- .../main/java/com/tailscale/ipn/IPNService.kt | 29 ++++++-- 2 files changed, 68 insertions(+), 31 deletions(-) diff --git a/android/src/main/java/com/tailscale/ipn/App.kt b/android/src/main/java/com/tailscale/ipn/App.kt index dd89972..b157095 100644 --- a/android/src/main/java/com/tailscale/ipn/App.kt +++ b/android/src/main/java/com/tailscale/ipn/App.kt @@ -36,6 +36,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -141,17 +143,32 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner { initViewModels() applicationScope.launch { Notifier.state.collect { state -> - val ableToStartVPN = state > Ipn.State.NeedsMachineAuth - // If VPN is stopped, show a disconnected notification. If it is running as a foregrround - // service, IPNService will show a connected notification. - if (state == Ipn.State.Stopped) { - notifyStatus(false) - } - val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running - updateConnStatus(ableToStartVPN) - QuickToggleService.setVPNRunning(vpnRunning) + combine(Notifier.state, MDMSettings.forceEnabled.flow) { state, forceEnabled -> + Pair(state, forceEnabled) + } + .collect { (state, hideDisconnectAction) -> + val ableToStartVPN = state > Ipn.State.NeedsMachineAuth + // If VPN is stopped, show a disconnected notification. If it is running as a + // foreground + // service, IPNService will show a connected notification. + if (state == Ipn.State.Stopped) { + notifyStatus(vpnRunning = false, hideDisconnectAction = hideDisconnectAction.value) + } + + val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running + updateConnStatus(ableToStartVPN) + QuickToggleService.setVPNRunning(vpnRunning) + + // Update notification status when VPN is running + if (vpnRunning) { + notifyStatus(vpnRunning = true, hideDisconnectAction = hideDisconnectAction.value) + } + } } } + applicationScope.launch { + val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() + } } private fun initViewModels() { @@ -408,8 +425,8 @@ open class UninitializedApp : Application() { notificationManager.createNotificationChannel(channel) } - fun notifyStatus(vpnRunning: Boolean) { - notifyStatus(buildStatusNotification(vpnRunning)) + fun notifyStatus(vpnRunning: Boolean, hideDisconnectAction: Boolean) { + notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction)) } fun notifyStatus(notification: Notification) { @@ -427,7 +444,7 @@ open class UninitializedApp : Application() { notificationManager.notify(STATUS_NOTIFICATION_ID, notification) } - fun buildStatusNotification(vpnRunning: Boolean): Notification { + fun buildStatusNotification(vpnRunning: Boolean, hideDisconnectAction: Boolean): Notification { val message = getString(if (vpnRunning) R.string.connected else R.string.not_connected) val icon = if (vpnRunning) R.drawable.ic_notification else R.drawable.ic_notification_disabled val action = @@ -449,19 +466,22 @@ open class UninitializedApp : Application() { PendingIntent.getActivity( this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - return NotificationCompat.Builder(this, STATUS_CHANNEL_ID) - .setSmallIcon(icon) - .setContentTitle("Tailscale") - .setContentText(message) - .setAutoCancel(!vpnRunning) - .setOnlyAlertOnce(!vpnRunning) - .setOngoing(vpnRunning) - .setSilent(true) - .setOngoing(false) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .addAction(NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build()) - .setContentIntent(pendingIntent) - .build() + val builder = + NotificationCompat.Builder(this, STATUS_CHANNEL_ID) + .setSmallIcon(icon) + .setContentTitle(getString(R.string.app_name)) + .setContentText(message) + .setAutoCancel(!vpnRunning) + .setOnlyAlertOnce(!vpnRunning) + .setOngoing(vpnRunning) + .setSilent(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + if (!vpnRunning || !hideDisconnectAction) { + builder.addAction( + NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build()) + } + return builder.build() } fun addUserDisallowedPackageName(packageName: String) { diff --git a/android/src/main/java/com/tailscale/ipn/IPNService.kt b/android/src/main/java/com/tailscale/ipn/IPNService.kt index db0ca06..920d08d 100644 --- a/android/src/main/java/com/tailscale/ipn/IPNService.kt +++ b/android/src/main/java/com/tailscale/ipn/IPNService.kt @@ -12,6 +12,10 @@ import com.tailscale.ipn.mdm.MDMSettings import com.tailscale.ipn.ui.model.Ipn import com.tailscale.ipn.ui.notifier.Notifier import com.tailscale.ipn.util.TSLog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import libtailscale.Libtailscale import java.util.UUID @@ -19,6 +23,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { private val TAG = "IPNService" private val randomID: String = UUID.randomUUID().toString() private lateinit var app: App + val scope = CoroutineScope(Dispatchers.IO) override fun id(): String { return randomID @@ -42,7 +47,11 @@ open class IPNService : VpnService(), libtailscale.IPNService { START_NOT_STICKY } ACTION_START_VPN -> { - showForegroundNotification() + scope.launch { + // Collect the first value of hideDisconnectAction asynchronously. + val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() + showForegroundNotification(hideDisconnectAction.value) + } app.setWantRunning(true) Libtailscale.requestVPN(this) START_STICKY @@ -51,7 +60,11 @@ open class IPNService : VpnService(), libtailscale.IPNService { // This means we were started by Android due to Always On VPN. // We show a non-foreground notification because we weren't // started as a foreground service. - app.notifyStatus(true) + scope.launch { + // Collect the first value of hideDisconnectAction asynchronously. + val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() + app.notifyStatus(true, hideDisconnectAction.value) + } app.setWantRunning(true) Libtailscale.requestVPN(this) START_STICKY @@ -60,7 +73,11 @@ 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()) { - showForegroundNotification() + scope.launch { + // Collect the first value of hideDisconnectAction asynchronously. + val hideDisconnectAction = MDMSettings.forceEnabled.flow.first() + showForegroundNotification(hideDisconnectAction.value) + } App.get() Libtailscale.requestVPN(this) START_STICKY @@ -77,7 +94,7 @@ open class IPNService : VpnService(), libtailscale.IPNService { Libtailscale.serviceDisconnect(this) } - override fun disconnectVPN(){ + override fun disconnectVPN() { stopSelf() } @@ -97,11 +114,11 @@ open class IPNService : VpnService(), libtailscale.IPNService { app.getAppScopedViewModel().setVpnPrepared(isPrepared) } - private fun showForegroundNotification() { + private fun showForegroundNotification(hideDisconnectAction: Boolean) { try { startForeground( UninitializedApp.STATUS_NOTIFICATION_ID, - UninitializedApp.get().buildStatusNotification(true)) + UninitializedApp.get().buildStatusNotification(true, hideDisconnectAction)) } catch (e: Exception) { TSLog.e(TAG, "Failed to start foreground service: $e") }