Skip to content

Commit af98b14

Browse files
authored
android: hide disconnect action if force enabled (#539)
In notification, don't show 'Disconnect' button if MDM force enable is on. Fixes tailscale/corp#23764 Signed-off-by: kari-ts <[email protected]>
1 parent 2e9f6b7 commit af98b14

File tree

2 files changed

+68
-31
lines changed

2 files changed

+68
-31
lines changed

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import kotlinx.coroutines.CoroutineScope
3636
import kotlinx.coroutines.Dispatchers
3737
import kotlinx.coroutines.SupervisorJob
3838
import kotlinx.coroutines.cancel
39+
import kotlinx.coroutines.flow.combine
40+
import kotlinx.coroutines.flow.first
3941
import kotlinx.coroutines.launch
4042
import kotlinx.serialization.encodeToString
4143
import kotlinx.serialization.json.Json
@@ -141,17 +143,32 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
141143
initViewModels()
142144
applicationScope.launch {
143145
Notifier.state.collect { state ->
144-
val ableToStartVPN = state > Ipn.State.NeedsMachineAuth
145-
// If VPN is stopped, show a disconnected notification. If it is running as a foregrround
146-
// service, IPNService will show a connected notification.
147-
if (state == Ipn.State.Stopped) {
148-
notifyStatus(false)
149-
}
150-
val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running
151-
updateConnStatus(ableToStartVPN)
152-
QuickToggleService.setVPNRunning(vpnRunning)
146+
combine(Notifier.state, MDMSettings.forceEnabled.flow) { state, forceEnabled ->
147+
Pair(state, forceEnabled)
148+
}
149+
.collect { (state, hideDisconnectAction) ->
150+
val ableToStartVPN = state > Ipn.State.NeedsMachineAuth
151+
// If VPN is stopped, show a disconnected notification. If it is running as a
152+
// foreground
153+
// service, IPNService will show a connected notification.
154+
if (state == Ipn.State.Stopped) {
155+
notifyStatus(vpnRunning = false, hideDisconnectAction = hideDisconnectAction.value)
156+
}
157+
158+
val vpnRunning = state == Ipn.State.Starting || state == Ipn.State.Running
159+
updateConnStatus(ableToStartVPN)
160+
QuickToggleService.setVPNRunning(vpnRunning)
161+
162+
// Update notification status when VPN is running
163+
if (vpnRunning) {
164+
notifyStatus(vpnRunning = true, hideDisconnectAction = hideDisconnectAction.value)
165+
}
166+
}
153167
}
154168
}
169+
applicationScope.launch {
170+
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
171+
}
155172
}
156173

157174
private fun initViewModels() {
@@ -419,8 +436,8 @@ open class UninitializedApp : Application() {
419436
notificationManager.createNotificationChannel(channel)
420437
}
421438

422-
fun notifyStatus(vpnRunning: Boolean) {
423-
notifyStatus(buildStatusNotification(vpnRunning))
439+
fun notifyStatus(vpnRunning: Boolean, hideDisconnectAction: Boolean) {
440+
notifyStatus(buildStatusNotification(vpnRunning, hideDisconnectAction))
424441
}
425442

426443
fun notifyStatus(notification: Notification) {
@@ -438,7 +455,7 @@ open class UninitializedApp : Application() {
438455
notificationManager.notify(STATUS_NOTIFICATION_ID, notification)
439456
}
440457

441-
fun buildStatusNotification(vpnRunning: Boolean): Notification {
458+
fun buildStatusNotification(vpnRunning: Boolean, hideDisconnectAction: Boolean): Notification {
442459
val message = getString(if (vpnRunning) R.string.connected else R.string.not_connected)
443460
val icon = if (vpnRunning) R.drawable.ic_notification else R.drawable.ic_notification_disabled
444461
val action =
@@ -460,19 +477,22 @@ open class UninitializedApp : Application() {
460477
PendingIntent.getActivity(
461478
this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
462479

463-
return NotificationCompat.Builder(this, STATUS_CHANNEL_ID)
464-
.setSmallIcon(icon)
465-
.setContentTitle("Tailscale")
466-
.setContentText(message)
467-
.setAutoCancel(!vpnRunning)
468-
.setOnlyAlertOnce(!vpnRunning)
469-
.setOngoing(vpnRunning)
470-
.setSilent(true)
471-
.setOngoing(false)
472-
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
473-
.addAction(NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build())
474-
.setContentIntent(pendingIntent)
475-
.build()
480+
val builder =
481+
NotificationCompat.Builder(this, STATUS_CHANNEL_ID)
482+
.setSmallIcon(icon)
483+
.setContentTitle(getString(R.string.app_name))
484+
.setContentText(message)
485+
.setAutoCancel(!vpnRunning)
486+
.setOnlyAlertOnce(!vpnRunning)
487+
.setOngoing(vpnRunning)
488+
.setSilent(true)
489+
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
490+
.setContentIntent(pendingIntent)
491+
if (!vpnRunning || !hideDisconnectAction) {
492+
builder.addAction(
493+
NotificationCompat.Action.Builder(0, actionLabel, pendingButtonIntent).build())
494+
}
495+
return builder.build()
476496
}
477497

478498
fun addUserDisallowedPackageName(packageName: String) {

android/src/main/java/com/tailscale/ipn/IPNService.kt

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ import com.tailscale.ipn.mdm.MDMSettings
1212
import com.tailscale.ipn.ui.model.Ipn
1313
import com.tailscale.ipn.ui.notifier.Notifier
1414
import com.tailscale.ipn.util.TSLog
15+
import kotlinx.coroutines.CoroutineScope
16+
import kotlinx.coroutines.Dispatchers
17+
import kotlinx.coroutines.flow.first
18+
import kotlinx.coroutines.launch
1519
import libtailscale.Libtailscale
1620
import java.util.UUID
1721

1822
open class IPNService : VpnService(), libtailscale.IPNService {
1923
private val TAG = "IPNService"
2024
private val randomID: String = UUID.randomUUID().toString()
2125
private lateinit var app: App
26+
val scope = CoroutineScope(Dispatchers.IO)
2227

2328
override fun id(): String {
2429
return randomID
@@ -42,7 +47,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
4247
START_NOT_STICKY
4348
}
4449
ACTION_START_VPN -> {
45-
showForegroundNotification()
50+
scope.launch {
51+
// Collect the first value of hideDisconnectAction asynchronously.
52+
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
53+
showForegroundNotification(hideDisconnectAction.value)
54+
}
4655
app.setWantRunning(true)
4756
Libtailscale.requestVPN(this)
4857
START_STICKY
@@ -51,7 +60,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
5160
// This means we were started by Android due to Always On VPN.
5261
// We show a non-foreground notification because we weren't
5362
// started as a foreground service.
54-
app.notifyStatus(true)
63+
scope.launch {
64+
// Collect the first value of hideDisconnectAction asynchronously.
65+
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
66+
app.notifyStatus(true, hideDisconnectAction.value)
67+
}
5568
app.setWantRunning(true)
5669
Libtailscale.requestVPN(this)
5770
START_STICKY
@@ -60,7 +73,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
6073
// This means that we were restarted after the service was killed
6174
// (potentially due to OOM).
6275
if (UninitializedApp.get().isAbleToStartVPN()) {
63-
showForegroundNotification()
76+
scope.launch {
77+
// Collect the first value of hideDisconnectAction asynchronously.
78+
val hideDisconnectAction = MDMSettings.forceEnabled.flow.first()
79+
showForegroundNotification(hideDisconnectAction.value)
80+
}
6481
App.get()
6582
Libtailscale.requestVPN(this)
6683
START_STICKY
@@ -77,7 +94,7 @@ open class IPNService : VpnService(), libtailscale.IPNService {
7794
Libtailscale.serviceDisconnect(this)
7895
}
7996

80-
override fun disconnectVPN(){
97+
override fun disconnectVPN() {
8198
stopSelf()
8299
}
83100

@@ -97,11 +114,11 @@ open class IPNService : VpnService(), libtailscale.IPNService {
97114
app.getAppScopedViewModel().setVpnPrepared(isPrepared)
98115
}
99116

100-
private fun showForegroundNotification() {
117+
private fun showForegroundNotification(hideDisconnectAction: Boolean) {
101118
try {
102119
startForeground(
103120
UninitializedApp.STATUS_NOTIFICATION_ID,
104-
UninitializedApp.get().buildStatusNotification(true))
121+
UninitializedApp.get().buildStatusNotification(true, hideDisconnectAction))
105122
} catch (e: Exception) {
106123
TSLog.e(TAG, "Failed to start foreground service: $e")
107124
}

0 commit comments

Comments
 (0)